class CaseRepeater(Repeater): """ Record that cases should be repeated to a new url """ payload_generator_classes = (CaseRepeaterXMLPayloadGenerator, CaseRepeaterJsonPayloadGenerator) version = StringProperty(default=V2, choices=LEGAL_VERSIONS) white_listed_case_types = StringListProperty( default=[]) # empty value means all case-types are accepted black_listed_users = StringListProperty( default=[]) # users who caseblock submissions should be ignored friendly_name = _("Forward Cases") def allowed_to_forward(self, payload): return self._allowed_case_type(payload) and self._allowed_user(payload) def _allowed_case_type(self, payload): return not self.white_listed_case_types or payload.type in self.white_listed_case_types def _allowed_user(self, payload): return self.payload_user_id(payload) not in self.black_listed_users def payload_user_id(self, payload): # get the user_id who submitted the payload, note, it's not the owner_id return payload.actions[-1].user_id @memoized def payload_doc(self, repeat_record): return CaseAccessors(repeat_record.domain).get_case( repeat_record.payload_id) @property def form_class_name(self): """ CaseRepeater and most of its subclasses use the same form for editing """ return 'CaseRepeater' def get_headers(self, repeat_record): headers = super(CaseRepeater, self).get_headers(repeat_record) headers.update({ "server-modified-on": self.payload_doc(repeat_record).server_modified_on.isoformat() + "Z" }) return headers def __str__(self): return "forwarding cases to: %s" % self.url
class InternalProperties(DocumentSchema, UpdatableSchema): """ Project properties that should only be visible/editable by superusers """ sf_contract_id = StringProperty() sf_account_id = StringProperty() commcare_edition = StringProperty( choices=['', "plus", "community", "standard", "pro", "advanced", "enterprise"], default="community" ) initiative = StringListProperty() workshop_region = StringProperty() project_state = StringProperty(choices=["", "POC", "transition", "at-scale"], default="") self_started = BooleanProperty(default=True) area = StringProperty() sub_area = StringProperty() using_adm = BooleanProperty() using_call_center = BooleanProperty() custom_eula = BooleanProperty() can_use_data = BooleanProperty(default=True) notes = StringProperty() organization_name = StringProperty() platform = StringListProperty() project_manager = StringProperty() phone_model = StringProperty() goal_time_period = IntegerProperty() goal_followup_rate = DecimalProperty() # intentionally different from and commtrack_enabled so that FMs can change commtrack_domain = BooleanProperty() performance_threshold = IntegerProperty() experienced_threshold = IntegerProperty() amplifies_workers = StringProperty( choices=[AMPLIFIES_YES, AMPLIFIES_NO, AMPLIFIES_NOT_SET], default=AMPLIFIES_NOT_SET ) amplifies_project = StringProperty( choices=[AMPLIFIES_YES, AMPLIFIES_NO, AMPLIFIES_NOT_SET], default=AMPLIFIES_NOT_SET ) business_unit = StringProperty(choices=BUSINESS_UNITS + [""], default="") data_access_threshold = IntegerProperty() partner_technical_competency = IntegerProperty() support_prioritization = IntegerProperty() gs_continued_involvement = StringProperty() technical_complexity = StringProperty() app_design_comments = StringProperty() training_materials = StringProperty() partner_comments = StringProperty() partner_contact = StringProperty() dimagi_contact = StringProperty()
class CustomDataField(JsonObject): slug = StringProperty() is_required = BooleanProperty() label = StringProperty() choices = StringListProperty() regex = StringProperty() regex_msg = StringProperty()
class Deployment(DocumentSchema, UpdatableSchema): city = StringProperty() countries = StringListProperty() region = StringProperty( ) # e.g. US, LAC, SA, Sub-saharn Africa, East Africa, West Africa, Southeast Asia) description = StringProperty() public = BooleanProperty(default=False)
class CustomDataField(JsonObject): slug = StringProperty() is_required = BooleanProperty() label = StringProperty() choices = StringListProperty() is_multiple_choice = BooleanProperty(default=False) # Currently only relevant for location fields index_in_fixture = BooleanProperty(default=False)
class GroupExportConfiguration(Document): """ An export configuration allows you to setup a collection of exports that all run together. Used by the management command or a scheduled job to run a bunch of exports on a schedule. """ full_exports = SchemaListProperty(ExportConfiguration) custom_export_ids = StringListProperty() def get_custom_exports(self): for custom in list(self.custom_export_ids): custom_export = self._get_custom(custom) if custom_export: yield custom_export def _get_custom(self, custom_id): """ Get a custom export, or delete it's reference if not found """ try: return SavedExportSchema.get(custom_id) except ResourceNotFound: try: self.custom_export_ids.remove(custom_id) self.save() except ValueError: pass @property @memoized def all_configs(self): """ Return an iterator of config-like objects that include the main configs + the custom export configs. """ return [full for full in self.full_exports] + \ [custom.to_export_config() for custom in self.get_custom_exports()] @property def all_export_schemas(self): """ Return an iterator of ExportSchema-like objects that include the main configs + the custom export configs. """ for full in self.full_exports: yield DefaultExportSchema(index=full.index, type=full.type) for custom in self.get_custom_exports(): yield custom @property @memoized def all_exports(self): """ Returns an iterator of tuples consisting of the export config and an ExportSchema-like document that can be used to get at the data. """ return list(zip(self.all_configs, self.all_export_schemas))
class SplitColumn(ComplexExportColumn): """ This class is used to split a value into multiple columns based on a set of pre-defined options. It splits the data value assuming it is space separated. The outputs will have one column for each 'option' and one additional column for any values from the data don't appear in the options. Each column will have a value of 1 if the data value contains the option for that column otherwise the column will be blank. e.g. options = ['a', 'b'] column_headers = ['col a', 'col b', 'col extra'] data_val = 'a c d' output = [1, '', 'c d'] """ options = StringListProperty() ignore_extras = False def get_headers(self): header = self.display if '{option}' in self.display else "{name} | {option}" for option in self.options: yield header.format(name=self.display, option=option) if not self.ignore_extras: yield header.format(name=self.display, option='extra') def get_data(self, value): from couchexport.export import Constant opts_len = len(self.options) if isinstance(value, Constant): row = [value] * opts_len else: row = [None] * opts_len if not isinstance(value, six.string_types): return row if self.ignore_extras else row + [value] soft_assert_type_text(value) values = value.split(' ') if value else [] for index, option in enumerate(self.options): if option in values: row[index] = 1 values.remove(option) if self.ignore_extras: return row else: remainder = ' '.join(values) if values else None return row + [remainder] def to_config_format(self, selected=True): config = super(SplitColumn, self).to_config_format(selected) config['options'] = self.options return config
class FormRepeater(Repeater): """ Record that forms should be repeated to a new url """ payload_generator_classes = (FormRepeaterXMLPayloadGenerator, FormRepeaterJsonPayloadGenerator) include_app_id_param = BooleanProperty(default=True) white_listed_form_xmlns = StringListProperty( default=[]) # empty value means all form xmlns are accepted friendly_name = _("Forward Forms") @memoized def payload_doc(self, repeat_record): return FormAccessors(repeat_record.domain).get_form( repeat_record.payload_id) @property def form_class_name(self): """ FormRepeater and its subclasses use the same form for editing """ return 'FormRepeater' def allowed_to_forward(self, payload): return (payload.xmlns != DEVICE_LOG_XMLNS and (not self.white_listed_form_xmlns or payload.xmlns in self.white_listed_form_xmlns)) def get_url(self, repeat_record): url = super(FormRepeater, self).get_url(repeat_record) if not self.include_app_id_param: return url else: # adapted from http://stackoverflow.com/a/2506477/10840 url_parts = list(urlparse(url)) query = parse_qsl(url_parts[4]) try: query.append( ("app_id", self.payload_doc(repeat_record).app_id)) except (XFormNotFound, ResourceNotFound): return None url_parts[4] = urlencode(query) return urlunparse(url_parts) def get_headers(self, repeat_record): headers = super(FormRepeater, self).get_headers(repeat_record) headers.update({ "received-on": self.payload_doc(repeat_record).received_on.isoformat() + "Z" }) return headers def __str__(self): return "forwarding forms to: %s" % self.url
class ModelAuditEvent(models.Model): object_type = StringProperty() # String of ContentType/Model, verbose_name='Case linking content type', blank=True, null=True) object_uuid = StringProperty() #('object_uuid', max_length=32, db_index=True, blank=True, null=True) properties = StringListProperty() #models.ManyToManyField(FieldAccess, blank=True, null=True) property_data = DictProperty() #models.TextField() #json of the actual fields accessed user = StringProperty() # The User's username accessing this accessed = DateTimeProperty(default=getdate) class Meta: app_label = 'auditcare'
class CommCareMultimedia(BlobMixin, SafeSaveDocument): """ The base object of all CommCare Multimedia """ file_hash = StringProperty() # use this to search for multimedia in couch aux_media = SchemaListProperty(AuxMedia) last_modified = DateTimeProperty() valid_domains = StringListProperty( ) # A list of domains that uses this file owners = StringListProperty( default=[]) # list of domains that uploaded this file licenses = SchemaListProperty(HQMediaLicense, default=[]) shared_by = StringListProperty( default=[]) # list of domains that can share this file tags = DictProperty(default={}) # dict of string lists _blobdb_type_code = CODES.multimedia @classmethod def get(cls, docid, rev=None, db=None, dynamic_properties=True): # copied and tweaked from the superclass's method if not db: db = cls.get_db() cls._allow_dynamic_properties = dynamic_properties # on cloudant don't get the doc back until all nodes agree # on the copy, to avoid race conditions extras = get_safe_read_kwargs() return db.get(docid, rev=rev, wrapper=cls.wrap, **extras) @property def is_shared(self): return len(self.shared_by) > 0 @property def license(self): return self.licenses[0] if self.licenses else None @retry_resource(3) def update_or_add_license(self, domain, type="", author="", attribution_notes="", org="", should_save=True): for license in self.licenses: if license.domain == domain: license.type = type or license.type license.author = author or license.author license.organization = org or license.organization license.attribution_notes = attribution_notes or license.attribution_notes break else: license = HQMediaLicense(domain=domain, type=type, author=author, attribution_notes=attribution_notes, organization=org) self.licenses.append(license) if should_save: self.save() def url(self): return reverse("hqmedia_download", args=[self.doc_type, self._id]) def attach_data(self, data, original_filename=None, username=None, attachment_id=None, media_meta=None): """ This creates the auxmedia attachment with the downloaded data. """ self.last_modified = datetime.utcnow() if not attachment_id: attachment_id = self.file_hash if not self.blobs or attachment_id not in self.blobs: if not getattr(self, '_id'): # put_attchment blows away existing data, so make sure an id has been assigned # to this guy before we do it. This is the usual path; remote apps are the exception. self.save() self.put_attachment( data, attachment_id, content_type=self.get_mime_type(data, filename=original_filename), domain=SHARED_DOMAIN, ) new_media = AuxMedia() new_media.uploaded_date = datetime.utcnow() new_media.attachment_id = attachment_id new_media.uploaded_filename = original_filename new_media.uploaded_by = username new_media.checksum = self.file_hash if media_meta: new_media.media_meta = media_meta self.aux_media.append(new_media) self.save() return True def add_domain(self, domain, owner=None, **kwargs): if len(self.owners) == 0: # this is intended to simulate migration--if it happens that a media file somehow gets no more owners # (which should be impossible) it will transfer ownership to all copiers... not necessarily a bad thing, # just something to be aware of self.owners = self.valid_domains if owner and domain not in self.owners: self.owners.append(domain) elif not owner and domain in self.owners: self.owners.remove(domain) if domain in self.owners: shared = kwargs.get('shared', '') if shared and domain not in self.shared_by: self.shared_by.append(domain) elif not shared and shared != '' and domain in self.shared_by: self.shared_by.remove(domain) if kwargs.get('tags', ''): self.tags[domain] = kwargs['tags'] if domain not in self.valid_domains: self.valid_domains.append(domain) self.save() def get_display_file(self, return_type=True): if self.attachment_id: data = self.fetch_attachment(self.attachment_id, stream=True).read() if return_type: content_type = self.blobs[self.attachment_id].content_type return data, content_type else: return data return None def get_file_extension(self): extension = '' if self.aux_media: extension = '.{}'.format( self.aux_media[-1]['uploaded_filename'].split(".")[-1]) return extension def get_media_info(self, path, is_updated=False, original_path=None): return { "path": path, "uid": self.file_hash, "m_id": self._id, "url": reverse("hqmedia_download", args=[self.__class__.__name__, self._id]), "updated": is_updated, "original_path": original_path, "icon_class": self.get_icon_class(), "media_type": self.get_nice_name(), "humanized_content_length": filesizeformat(self.content_length), } @property def attachment_id(self): if not self.aux_media: return None ids = set([aux.attachment_id for aux in self.aux_media]) assert len(ids) == 1 return ids.pop() @property def content_length(self): if self.attachment_id: return self.blobs[self.attachment_id].content_length @classmethod def get_mime_type(cls, data, filename=None): mime = magic.Magic(mime=True) mime_type = mime.from_buffer(data) if mime_type.startswith('application') and filename is not None: guessed_type = mimetypes.guess_type(filename) mime_type = guessed_type[0] if guessed_type[0] else mime_type return mime_type @classmethod def get_base_mime_type(cls, data, filename=None): mime_type = cls.get_mime_type(data, filename=filename) return mime_type.split('/')[0] if mime_type else None @classmethod def generate_hash(cls, data): return hashlib.md5(data).hexdigest() @classmethod def get_by_hash(cls, file_hash): result = cls.view('hqmedia/by_hash', key=file_hash, include_docs=True).first() if not result: result = cls() result.file_hash = file_hash return result @classmethod def get_by_data(cls, data): file_hash = cls.generate_hash(data) return cls.get_by_hash(file_hash) @classmethod def get_doc_types(cls): return ('CommCareImage', 'CommCareAudio', 'CommCareVideo') @classmethod def get_doc_class(cls, doc_type): return { 'CommCareImage': CommCareImage, 'CommCareAudio': CommCareAudio, 'CommCareVideo': CommCareVideo, 'CommCareMultimedia': CommCareMultimedia, }[doc_type] @classmethod def get_class_by_data(cls, data, filename=None): return { 'image': CommCareImage, 'audio': CommCareAudio, 'video': CommCareVideo, }.get(cls.get_base_mime_type(data, filename=filename)) @classmethod def get_form_path(cls, path, lowercase=False): path = path.strip() if lowercase: path = path.lower() if path.startswith(MULTIMEDIA_PREFIX): return path if path.startswith('/'): path = path[1:] return "%s%s" % (MULTIMEDIA_PREFIX, path) @classmethod def get_standard_path(cls, path): return path.replace(MULTIMEDIA_PREFIX, "") @classmethod def wrap(cls, data): should_save = False if data.get('tags') == []: data['tags'] = {} if not data.get('owners'): data['owners'] = data.get('valid_domains', []) if isinstance(data.get('licenses', ''), dict): # need to migrate licncses from old format to new format # old: {"mydomain": "public", "yourdomain": "cc"} migrated = [HQMediaLicense(domain=domain, type=type)._doc \ for domain, type in data["licenses"].items()] data['licenses'] = migrated # deprecating support for public domain license if isinstance(data.get("licenses", ""), list) and len(data["licenses"]) > 0: if data["licenses"][0].get("type", "") == "public": data["licenses"][0]["type"] = "cc" should_save = True self = super(CommCareMultimedia, cls).wrap(data) if should_save: self.save() return self @classmethod def get_nice_name(cls): return _("Generic Multimedia") @classmethod def get_icon_class(cls): return "fa fa-desktop"
class MirroredEngineIds(DocumentSchema): server_environment = StringProperty() engine_ids = StringListProperty()
class AccessAudit(AuditEvent): access_type = StringProperty(choices=ACCESS_CHOICES) ip_address = StringProperty() session_key = StringProperty() #the django auth session key user_agent = StringProperty() get_data = StringListProperty() post_data = StringListProperty() http_accept = StringProperty() path_info = StringProperty() failures_since_start = IntegerProperty() class Meta(object): app_label = 'auditcare' @property def summary(self): return "%s from %s" % (self.access_type, self.ip_address) @classmethod def audit_login(cls, request, user, *args, **kwargs): '''Creates an instance of a Access log. ''' audit = cls.create_audit(cls, user) audit.ip_address = utils.get_ip(request) ua = request.META.get('HTTP_USER_AGENT', '<unknown>') audit.http_accept = request.META.get('HTTP_ACCEPT', '<unknown>') audit.path_info = request.META.get('PATH_INFO', '<unknown>') audit.user_agent = ua audit.access_type = 'login' audit.description = "Login Success" audit.session_key = request.session.session_key audit.get_data = [] #[query2str(request.GET.items())] audit.post_data = [] audit.save() @classmethod def audit_login_failed(cls, request, username, *args, **kwargs): '''Creates an instance of a Access log. ''' audit = cls.create_audit(cls, username) audit.ip_address = utils.get_ip(request) audit.access_type = 'login_failed' if username != None: audit.description = "Login Failure: %s" % (username) else: audit.description = "Login Failure" audit.session_key = request.session.session_key audit.save() @classmethod def audit_logout(cls, request, user): '''Log a logout event''' audit = cls.create_audit(cls, user) audit.ip_address = utils.get_ip(request) if user == AnonymousUser: audit.description = "Logout anonymous" elif user is None: audit.description = "None" else: audit.description = "Logout %s" % (user.username) audit.access_type = 'logout' audit.session_key = request.session.session_key audit.save()
class Domain(QuickCachedDocumentMixin, Document, SnapshotMixin): """Domain is the highest level collection of people/stuff in the system. Pretty much everything happens at the domain-level, including user membership, permission to see data, reports, charts, etc.""" name = StringProperty() is_active = BooleanProperty() date_created = DateTimeProperty() default_timezone = StringProperty( default=getattr(settings, "TIME_ZONE", "UTC")) case_sharing = BooleanProperty(default=False) secure_submissions = BooleanProperty(default=False) cloudcare_releases = StringProperty( choices=['stars', 'nostars', 'default'], default='default') organization = StringProperty() hr_name = StringProperty() # the human-readable name for this project creating_user = StringProperty( ) # username of the user who created this domain # domain metadata project_type = StringProperty() # e.g. MCH, HIV customer_type = StringProperty() # plus, full, etc. is_test = StringProperty(choices=["true", "false", "none"], default="none") description = StringProperty() short_description = StringProperty() is_shared = BooleanProperty(default=False) commtrack_enabled = BooleanProperty(default=False) call_center_config = SchemaProperty(CallCenterProperties) has_careplan = BooleanProperty(default=False) restrict_superusers = BooleanProperty(default=False) allow_domain_requests = BooleanProperty(default=False) location_restriction_for_users = BooleanProperty(default=False) usercase_enabled = BooleanProperty(default=False) hipaa_compliant = BooleanProperty(default=False) use_sql_backend = BooleanProperty(default=False) case_display = SchemaProperty(CaseDisplaySettings) # CommConnect settings commconnect_enabled = BooleanProperty(default=False) survey_management_enabled = BooleanProperty(default=False) # Whether or not a case can register via sms sms_case_registration_enabled = BooleanProperty(default=False) # Case type to apply to cases registered via sms sms_case_registration_type = StringProperty() # Owner to apply to cases registered via sms sms_case_registration_owner_id = StringProperty() # Submitting user to apply to cases registered via sms sms_case_registration_user_id = StringProperty() # Whether or not a mobile worker can register via sms sms_mobile_worker_registration_enabled = BooleanProperty(default=False) use_default_sms_response = BooleanProperty(default=False) default_sms_response = StringProperty() chat_message_count_threshold = IntegerProperty() custom_chat_template = StringProperty( ) # See settings.CUSTOM_CHAT_TEMPLATES custom_case_username = StringProperty( ) # Case property to use when showing the case's name in a chat window # If empty, sms can be sent at any time. Otherwise, only send during # these windows of time. SMS_QUEUE_ENABLED must be True in localsettings # for this be considered. restricted_sms_times = SchemaListProperty(DayTimeWindow) # If empty, this is ignored. Otherwise, the framework will make sure # that during these days/times, no automated outbound sms will be sent # to someone if they have sent in an sms within sms_conversation_length # minutes. Outbound sms sent from a user in a chat window, however, will # still be sent. This is meant to prevent chat conversations from being # interrupted by automated sms reminders. # SMS_QUEUE_ENABLED must be True in localsettings for this to be # considered. sms_conversation_times = SchemaListProperty(DayTimeWindow) # In minutes, see above. sms_conversation_length = IntegerProperty(default=10) # Set to True to prevent survey questions and answers form being seen in # SMS chat windows. filter_surveys_from_chat = BooleanProperty(default=False) # The below option only matters if filter_surveys_from_chat = True. # If set to True, invalid survey responses will still be shown in the chat # window, while questions and valid responses will be filtered out. show_invalid_survey_responses_in_chat = BooleanProperty(default=False) # If set to True, if a message is read by anyone it counts as being read by # everyone. Set to False so that a message is only counted as being read # for a user if only that user has read it. count_messages_as_read_by_anyone = BooleanProperty(default=False) # Set to True to allow sending sms and all-label surveys to cases whose # phone number is duplicated with another contact send_to_duplicated_case_numbers = BooleanProperty(default=True) enable_registration_welcome_sms_for_case = BooleanProperty(default=False) enable_registration_welcome_sms_for_mobile_worker = BooleanProperty( default=False) sms_survey_date_format = StringProperty() # exchange/domain copying stuff is_snapshot = BooleanProperty(default=False) is_approved = BooleanProperty(default=False) snapshot_time = DateTimeProperty() published = BooleanProperty(default=False) license = StringProperty(choices=LICENSES, default='cc') title = StringProperty() cda = SchemaProperty(LicenseAgreement) multimedia_included = BooleanProperty(default=True) downloads = IntegerProperty( default=0) # number of downloads for this specific snapshot full_downloads = IntegerProperty( default=0) # number of downloads for all snapshots from this domain author = StringProperty() phone_model = StringProperty() attribution_notes = StringProperty() publisher = StringProperty(choices=["organization", "user"], default="user") yt_id = StringProperty() snapshot_head = BooleanProperty(default=False) deployment = SchemaProperty(Deployment) image_path = StringProperty() image_type = StringProperty() cached_properties = DictProperty() internal = SchemaProperty(InternalProperties) dynamic_reports = SchemaListProperty(DynamicReportSet) # extra user specified properties tags = StringListProperty() area = StringProperty(choices=AREA_CHOICES) sub_area = StringProperty(choices=SUB_AREA_CHOICES) launch_date = DateTimeProperty # to be eliminated from projects and related documents when they are copied for the exchange _dirty_fields = ('admin_password', 'admin_password_charset', 'city', 'countries', 'region', 'customer_type') default_mobile_worker_redirect = StringProperty(default=None) last_modified = DateTimeProperty(default=datetime(2015, 1, 1)) # when turned on, use SECURE_TIMEOUT for sessions of users who are members of this domain secure_sessions = BooleanProperty(default=False) two_factor_auth = BooleanProperty(default=False) strong_mobile_passwords = BooleanProperty(default=False) # There is no longer a way to request a report builder trial, so this property should be removed in the near # future. (Keeping it for now in case a user has requested a trial and but has not yet been granted it) requested_report_builder_trial = StringListProperty() requested_report_builder_subscription = StringListProperty() @classmethod def wrap(cls, data): # for domains that still use original_doc should_save = False if 'original_doc' in data: original_doc = data['original_doc'] del data['original_doc'] should_save = True if original_doc: original_doc = Domain.get_by_name(original_doc) data['copy_history'] = [original_doc._id] # for domains that have a public domain license if 'license' in data: if data.get("license", None) == "public": data["license"] = "cc" should_save = True if 'slug' in data and data["slug"]: data["hr_name"] = data["slug"] del data["slug"] if 'is_test' in data and isinstance(data["is_test"], bool): data["is_test"] = "true" if data["is_test"] else "false" should_save = True if 'cloudcare_releases' not in data: data['cloudcare_releases'] = 'nostars' # legacy default setting # Don't actually remove location_types yet. We can migrate fully and # remove this after everything's hunky-dory in production. 2015-03-06 if 'location_types' in data: data['obsolete_location_types'] = data.pop('location_types') self = super(Domain, cls).wrap(data) if self.deployment is None: self.deployment = Deployment() if should_save: self.save() return self def get_default_timezone(self): """return a timezone object from self.default_timezone""" import pytz return pytz.timezone(self.default_timezone) @staticmethod @quickcache(['name'], timeout=24 * 60 * 60) def is_secure_session_required(name): domain = Domain.get_by_name(name) return domain and domain.secure_sessions @staticmethod @skippable_quickcache(['couch_user._id', 'is_active'], skip_arg='strict', timeout=5 * 60, memoize_timeout=10) def active_for_couch_user(couch_user, is_active=True, strict=False): domain_names = couch_user.get_domains() return Domain.view( "domain/by_status", keys=[[is_active, d] for d in domain_names], reduce=False, include_docs=True, stale=settings.COUCH_STALE_QUERY if not strict else None, ).all() @staticmethod def active_for_user(user, is_active=True, strict=False): if isinstance(user, AnonymousUser): return [] from corehq.apps.users.models import CouchUser if isinstance(user, CouchUser): couch_user = user else: couch_user = CouchUser.from_django_user(user) if couch_user: return Domain.active_for_couch_user(couch_user, is_active=is_active, strict=strict) else: return [] @classmethod def field_by_prefix(cls, field, prefix=''): # unichr(0xfff8) is something close to the highest character available res = cls.view( "domain/fields_by_prefix", group=True, startkey=[field, True, prefix], endkey=[field, True, "%s%c" % (prefix, unichr(0xfff8)), {}]) vals = [(d['value'], d['key'][2]) for d in res] vals.sort(reverse=True) return [(v[1], v[0]) for v in vals] def add(self, model_instance, is_active=True): """ Add something to this domain, through the generic relation. Returns the created membership object """ # Add membership info to Couch couch_user = model_instance.get_profile().get_couch_user() couch_user.add_domain_membership(self.name) couch_user.save() def applications(self): return get_brief_apps_in_domain(self.name) def full_applications(self, include_builds=True): from corehq.apps.app_manager.models import Application, RemoteApp WRAPPERS = {'Application': Application, 'RemoteApp': RemoteApp} def wrap_application(a): return WRAPPERS[a['doc']['doc_type']].wrap(a['doc']) if include_builds: startkey = [self.name] endkey = [self.name, {}] else: startkey = [self.name, None] endkey = [self.name, None, {}] return Application.get_db().view('app_manager/applications', startkey=startkey, endkey=endkey, include_docs=True, wrapper=wrap_application).all() @cached_property def versions(self): apps = self.applications() return list(set(a.application_version for a in apps)) @cached_property def has_case_management(self): for app in self.full_applications(): if app.doc_type == 'Application': if app.has_case_management(): return True return False @cached_property def has_media(self): for app in self.full_applications(): if app.doc_type == 'Application' and app.has_media(): return True return False @property def use_cloudcare_releases(self): return self.cloudcare_releases != 'nostars' def all_users(self): from corehq.apps.users.models import CouchUser return CouchUser.by_domain(self.name) def recent_submissions(self): return domain_has_submission_in_last_30_days(self.name) @cached_property def languages(self): apps = self.applications() return set(chain.from_iterable([a.langs for a in apps])) def readable_languages(self): return ', '.join(lang_lookup[lang] or lang for lang in self.languages()) def __unicode__(self): return self.name @classmethod @skippable_quickcache(['name'], skip_arg='strict', timeout=30 * 60) def get_by_name(cls, name, strict=False): if not name: # get_by_name should never be called with name as None (or '', etc) # I fixed the code in such a way that if I raise a ValueError # all tests pass and basic pages load, # but in order not to break anything in the wild, # I'm opting to notify by email if/when this happens # but fall back to the previous behavior of returning None if settings.DEBUG: raise ValueError('%r is not a valid domain name' % name) else: _assert = soft_assert(notify_admins=True, exponential_backoff=False) _assert(False, '%r is not a valid domain name' % name) return None def _get_by_name(stale=False): extra_args = {'stale': settings.COUCH_STALE_QUERY} if stale else {} result = cls.view("domain/domains", key=name, reduce=False, include_docs=True, **extra_args).first() if not isinstance(result, Domain): # A stale view may return a result with no doc if the doc has just been deleted. # In this case couchdbkit just returns the raw view result as a dict return None else: return result domain = _get_by_name(stale=(not strict)) if domain is None and not strict: # on the off chance this is a brand new domain, try with strict domain = _get_by_name(stale=False) return domain @classmethod def get_or_create_with_name(cls, name, is_active=False, secure_submissions=True): result = cls.view("domain/domains", key=name, reduce=False, include_docs=True).first() if result: return result else: new_domain = Domain( name=name, is_active=is_active, date_created=datetime.utcnow(), secure_submissions=secure_submissions, ) new_domain.save(**get_safe_write_kwargs()) return new_domain @classmethod def generate_name(cls, hr_name, max_length=25): ''' Generate a URL-friendly name based on a given human-readable name. Normalizes given name, then looks for conflicting domains, addressing conflicts by adding "-1", "-2", etc. May return None if it fails to generate a new, unique name. Throws exception if it can't figure out a name, which shouldn't happen unless max_length is absurdly short. ''' name = name_to_url(hr_name, "project") if Domain.get_by_name(name): prefix = name while len(prefix): name = next_available_name( prefix, Domain.get_names_by_prefix(prefix + '-')) if Domain.get_by_name(name): # should never happen raise NameUnavailableException if len(name) <= max_length: return name prefix = prefix[:-1] raise NameUnavailableException return name @classmethod def get_all(cls, include_docs=True): domains = Domain.view("domain/not_snapshots", include_docs=False).all() if not include_docs: return domains else: return imap(cls.wrap, iter_docs(cls.get_db(), [d['id'] for d in domains])) @classmethod def get_all_names(cls): return [d['key'] for d in cls.get_all(include_docs=False)] @classmethod def get_all_ids(cls): return [d['id'] for d in cls.get_all(include_docs=False)] @classmethod def get_names_by_prefix(cls, prefix): return [ d['key'] for d in Domain.view("domain/domains", startkey=prefix, endkey=prefix + u"zzz", reduce=False, include_docs=False).all() ] def case_sharing_included(self): return self.case_sharing or reduce(lambda x, y: x or y, [ getattr(app, 'case_sharing', False) for app in self.applications() ], False) def save(self, **params): self.last_modified = datetime.utcnow() if not self._rev: # mark any new domain as timezone migration complete set_migration_complete(self.name) super(Domain, self).save(**params) from corehq.apps.domain.signals import commcare_domain_post_save results = commcare_domain_post_save.send_robust(sender='domain', domain=self) for result in results: # Second argument is None if there was no error if result[1]: notify_exception( None, message="Error occured during domain post_save %s: %s" % (self.name, str(result[1]))) def save_copy(self, new_domain_name=None, new_hr_name=None, user=None, copy_by_id=None, share_reminders=True, share_user_roles=True): from corehq.apps.app_manager.dbaccessors import get_app from corehq.apps.reminders.models import CaseReminderHandler from corehq.apps.fixtures.models import FixtureDataItem from corehq.apps.app_manager.dbaccessors import get_brief_apps_in_domain from corehq.apps.domain.dbaccessors import get_doc_ids_in_domain_by_class from corehq.apps.fixtures.models import FixtureDataType from corehq.apps.users.models import UserRole db = Domain.get_db() new_id = db.copy_doc(self.get_id)['id'] if new_domain_name is None: new_domain_name = new_id with CriticalSection( ['request_domain_name_{}'.format(new_domain_name)]): new_domain_name = Domain.generate_name(new_domain_name) new_domain = Domain.get(new_id) new_domain.name = new_domain_name new_domain.hr_name = new_hr_name new_domain.copy_history = self.get_updated_history() new_domain.is_snapshot = False new_domain.snapshot_time = None new_domain.organization = None # TODO: use current user's organization (?) # reset stuff new_domain.cda.signed = False new_domain.cda.date = None new_domain.cda.type = None new_domain.cda.user_id = None new_domain.cda.user_ip = None new_domain.is_test = "none" new_domain.internal = InternalProperties() new_domain.creating_user = user.username if user else None for field in self._dirty_fields: if hasattr(new_domain, field): delattr(new_domain, field) # Saving the domain should happen before we import any apps since # importing apps can update the domain object (for example, if user # as a case needs to be enabled) new_domain.save() new_app_components = {} # a mapping of component's id to its copy def copy_data_items(old_type_id, new_type_id): for item in FixtureDataItem.by_data_type( self.name, old_type_id): comp = self.copy_component(item.doc_type, item._id, new_domain_name, user=user) comp.data_type_id = new_type_id comp.save() def get_latest_app_id(doc_id): app = get_app(self.name, doc_id).get_latest_saved() if app: return app._id, app.doc_type for app in get_brief_apps_in_domain(self.name): doc_id, doc_type = app.get_id, app.doc_type original_doc_id = doc_id if copy_by_id and doc_id not in copy_by_id: continue if not self.is_snapshot: doc_id, doc_type = get_latest_app_id(doc_id) or (doc_id, doc_type) component = self.copy_component(doc_type, doc_id, new_domain_name, user=user) if component: new_app_components[original_doc_id] = component for doc_id in get_doc_ids_in_domain_by_class( self.name, FixtureDataType): if copy_by_id and doc_id not in copy_by_id: continue component = self.copy_component('FixtureDataType', doc_id, new_domain_name, user=user) copy_data_items(doc_id, component._id) if share_reminders: for doc_id in get_doc_ids_in_domain_by_class( self.name, CaseReminderHandler): self.copy_component('CaseReminderHandler', doc_id, new_domain_name, user=user) if share_user_roles: for doc_id in get_doc_ids_in_domain_by_class( self.name, UserRole): self.copy_component('UserRole', doc_id, new_domain_name, user=user) if user: def add_dom_to_user(user): user.add_domain_membership(new_domain_name, is_admin=True) apply_update(user, add_dom_to_user) def update_events(handler): """ Change the form_unique_id to the proper form for each event in a newly copied CaseReminderHandler """ from corehq.apps.app_manager.models import FormBase for event in handler.events: if not event.form_unique_id: continue form = FormBase.get_form(event.form_unique_id) form_app = form.get_app() m_index, f_index = form_app.get_form_location(form.unique_id) form_copy = new_app_components[form_app._id].get_module( m_index).get_form(f_index) event.form_unique_id = form_copy.unique_id def update_for_copy(handler): handler.active = False update_events(handler) if share_reminders: for handler in CaseReminderHandler.get_handlers(new_domain_name): apply_update(handler, update_for_copy) return new_domain def reminder_should_be_copied(self, handler): from corehq.apps.reminders.models import ON_DATETIME return (handler.start_condition_type != ON_DATETIME and handler.user_group_id is None) def copy_component(self, doc_type, id, new_domain_name, user=None): from corehq.apps.app_manager.models import import_app from corehq.apps.users.models import UserRole from corehq.apps.reminders.models import CaseReminderHandler from corehq.apps.fixtures.models import FixtureDataType, FixtureDataItem str_to_cls = { 'UserRole': UserRole, 'CaseReminderHandler': CaseReminderHandler, 'FixtureDataType': FixtureDataType, 'FixtureDataItem': FixtureDataItem, } if doc_type in ('Application', 'RemoteApp'): new_doc = import_app(id, new_domain_name) new_doc.copy_history.append(id) new_doc.case_sharing = False # when copying from app-docs that don't have # unique_id attribute on Modules new_doc.ensure_module_unique_ids(should_save=False) else: cls = str_to_cls[doc_type] db = cls.get_db() if doc_type == 'CaseReminderHandler': cur_doc = cls.get(id) if not self.reminder_should_be_copied(cur_doc): return None new_id = db.copy_doc(id)['id'] new_doc = cls.get(new_id) for field in self._dirty_fields: if hasattr(new_doc, field): delattr(new_doc, field) if hasattr(cls, '_meta_fields'): for field in cls._meta_fields: if not field.startswith('_') and hasattr(new_doc, field): delattr(new_doc, field) new_doc.domain = new_domain_name if doc_type == 'FixtureDataType': new_doc.copy_from = id new_doc.is_global = True if self.is_snapshot and doc_type == 'Application': new_doc.prepare_multimedia_for_exchange() new_doc.save() return new_doc def save_snapshot(self, share_reminders, copy_by_id=None): if self.is_snapshot: return self else: try: copy = self.save_copy(copy_by_id=copy_by_id, share_reminders=share_reminders, share_user_roles=False) except NameUnavailableException: return None copy.is_snapshot = True head = self.snapshots(limit=1).first() if head and head.snapshot_head: head.snapshot_head = False head.save() copy.snapshot_head = True copy.snapshot_time = datetime.utcnow() del copy.deployment copy.save() return copy def snapshots(self, **view_kwargs): return Domain.view('domain/snapshots', startkey=[self._id, {}], endkey=[self._id], include_docs=True, reduce=False, descending=True, **view_kwargs) @memoized def published_snapshot(self): snapshots = self.snapshots().all() for snapshot in snapshots: if snapshot.published: return snapshot return None def update_deployment(self, **kwargs): self.deployment.update(kwargs) self.save() def update_internal(self, **kwargs): self.internal.update(kwargs) self.save() def display_name(self): if self.is_snapshot: return "Snapshot of %s" % self.copied_from.display_name() return self.hr_name or self.name def long_display_name(self): if self.is_snapshot: return format_html("Snapshot of {}", self.copied_from.display_name()) return self.hr_name or self.name __str__ = long_display_name def get_license_display(self): return LICENSES.get(self.license) def get_license_url(self): return LICENSE_LINKS.get(self.license) def copies(self): return Domain.view('domain/copied_from_snapshot', key=self._id, include_docs=True) def copies_of_parent(self): return Domain.view('domain/copied_from_snapshot', keys=[s._id for s in self.copied_from.snapshots()], include_docs=True) def delete(self): self._pre_delete() super(Domain, self).delete() def _pre_delete(self): from corehq.apps.domain.signals import commcare_domain_pre_delete from corehq.apps.domain.deletion import apply_deletion_operations dynamic_deletion_operations = [] results = commcare_domain_pre_delete.send_robust(sender='domain', domain=self) for result in results: response = result[1] if isinstance(response, Exception): raise DomainDeleteException( u"Error occurred during domain pre_delete {}: {}".format( self.name, str(response))) elif response: assert isinstance(response, list) dynamic_deletion_operations.extend(response) # delete all associated objects for db, related_doc_ids in get_all_doc_ids_for_domain_grouped_by_db( self.name): iter_bulk_delete(db, related_doc_ids, chunksize=500) apply_deletion_operations(self.name, dynamic_deletion_operations) def all_media(self, from_apps=None): # todo add documentation or refactor from corehq.apps.hqmedia.models import CommCareMultimedia dom_with_media = self if not self.is_snapshot else self.copied_from if self.is_snapshot: app_ids = [ app.copied_from.get_id for app in self.full_applications() ] if from_apps: from_apps = set( [a_id for a_id in app_ids if a_id in from_apps]) else: from_apps = app_ids if from_apps: media = [] media_ids = set() apps = [ app for app in dom_with_media.full_applications() if app.get_id in from_apps ] for app in apps: if app.doc_type != 'Application': continue for _, m in app.get_media_objects(): if m.get_id not in media_ids: media.append(m) media_ids.add(m.get_id) return media return CommCareMultimedia.view('hqmedia/by_domain', key=dom_with_media.name, include_docs=True).all() def most_restrictive_licenses(self, apps_to_check=None): from corehq.apps.hqmedia.utils import most_restrictive licenses = [ m.license['type'] for m in self.all_media(from_apps=apps_to_check) if m.license ] return most_restrictive(licenses) @classmethod def get_module_by_name(cls, domain_name): """ import and return the python module corresponding to domain_name, or None if it doesn't exist. """ from corehq.apps.domain.utils import get_domain_module_map module_name = get_domain_module_map().get(domain_name, domain_name) try: return import_module(module_name) if module_name else None except ImportError: return None @property @memoized def commtrack_settings(self): # this import causes some dependency issues so lives in here from corehq.apps.commtrack.models import CommtrackConfig if self.commtrack_enabled: return CommtrackConfig.for_domain(self.name) else: return None @property def has_custom_logo(self): return (self['_attachments'] and LOGO_ATTACHMENT in self['_attachments']) def get_custom_logo(self): if not self.has_custom_logo: return None return (self.fetch_attachment(LOGO_ATTACHMENT), self['_attachments'][LOGO_ATTACHMENT]['content_type']) def get_case_display(self, case): """Get the properties display definition for a given case""" return self.case_display.case_details.get(case.type) def get_form_display(self, form): """Get the properties display definition for a given XFormInstance""" return self.case_display.form_details.get(form.xmlns) @property def total_downloads(self): """ Returns the total number of downloads from every snapshot created from this domain """ from corehq.apps.domain.dbaccessors import count_downloads_for_all_snapshots return count_downloads_for_all_snapshots(self.get_id) @property @memoized def download_count(self): """ Updates and returns the total number of downloads from every sister snapshot. """ if self.is_snapshot: self.full_downloads = self.copied_from.total_downloads return self.full_downloads @property @memoized def published_by(self): from corehq.apps.users.models import CouchUser pb_id = self.cda.user_id return CouchUser.get_by_user_id(pb_id) if pb_id else None @property def name_of_publisher(self): return self.published_by.human_friendly_name if self.published_by else "" @property def location_types(self): from corehq.apps.locations.models import LocationType return LocationType.objects.filter(domain=self.name).all() @memoized def has_privilege(self, privilege): from corehq.apps.accounting.utils import domain_has_privilege return domain_has_privilege(self, privilege) @property @memoized def uses_locations(self): from corehq import privileges from corehq.apps.locations.models import LocationType return (self.has_privilege(privileges.LOCATIONS) and (self.commtrack_enabled or LocationType.objects.filter(domain=self.name).exists())) @property def supports_multiple_locations_per_user(self): """ This method is a wrapper around the toggle that enables multiple location functionality. Callers of this method should know that this is special functionality left around for special applications, and not a feature flag that should be set normally. """ return toggles.MULTIPLE_LOCATIONS_PER_USER.enabled(self.name) def convert_to_commtrack(self): """ One-stop-shop to make a domain CommTrack """ from corehq.apps.commtrack.util import make_domain_commtrack make_domain_commtrack(self) def clear_caches(self): from .utils import domain_restricts_superusers super(Domain, self).clear_caches() self.get_by_name.clear(self.__class__, self.name) self.is_secure_session_required.clear(self.name) domain_restricts_superusers.clear(self.name)
class FixtureDataType(Document): domain = StringProperty() is_global = BooleanProperty(default=False) tag = StringProperty() fields = SchemaListProperty(FixtureTypeField) item_attributes = StringListProperty() description = StringProperty() copy_from = StringProperty() @classmethod def wrap(cls, obj): if not obj["doc_type"] == "FixtureDataType": raise ResourceNotFound # Migrate fixtures without attributes on item-fields to fields with attributes if obj["fields"] and isinstance(obj['fields'][0], basestring): obj['fields'] = [{ 'field_name': f, 'properties': [] } for f in obj['fields']] # Migrate fixtures without attributes on items to items with attributes if 'item_attributes' not in obj: obj['item_attributes'] = [] return super(FixtureDataType, cls).wrap(obj) # support for old fields @property def fields_without_attributes(self): fields_without_attributes = [] for fixt_field in self.fields: fields_without_attributes.append(fixt_field.field_name) return fields_without_attributes @classmethod def total_by_domain(cls, domain): from corehq.apps.fixtures.dbaccessors import \ get_number_of_fixture_data_types_in_domain return get_number_of_fixture_data_types_in_domain(domain) @classmethod def by_domain(cls, domain): from corehq.apps.fixtures.dbaccessors import \ get_fixture_data_types_in_domain return get_fixture_data_types_in_domain(domain) @classmethod def by_domain_tag(cls, domain, tag): return cls.view('fixtures/data_types_by_domain_tag', key=[domain, tag], reduce=False, include_docs=True, descending=True) @classmethod def fixture_tag_exists(cls, domain, tag): fdts = FixtureDataType.by_domain(domain) for fdt in fdts: if tag == fdt.tag: return fdt return False def recursive_delete(self, transaction): item_ids = [] for item in FixtureDataItem.by_data_type(self.domain, self.get_id): transaction.delete(item) item_ids.append(item.get_id) transaction.delete_all( FixtureOwnership.for_all_item_ids(item_ids, self.domain)) transaction.delete(self) @classmethod def delete_fixtures_by_domain(cls, domain, transaction): for type in FixtureDataType.by_domain(domain): type.recursive_delete(transaction)
class SQLColumnIndexes(DocumentSchema): column_ids = StringListProperty()
class CustomDataField(JsonObject): slug = StringProperty() is_required = BooleanProperty() label = StringProperty() choices = StringListProperty() is_multiple_choice = BooleanProperty(default=False)
class Domain(QuickCachedDocumentMixin, BlobMixin, Document, SnapshotMixin): """ Domain is the highest level collection of people/stuff in the system. Pretty much everything happens at the domain-level, including user membership, permission to see data, reports, charts, etc. Exceptions: accounting has some models that combine multiple domains, which make "enterprise" multi-domain features like the enterprise dashboard possible. Naming conventions: Most often, variables representing domain names are named `domain`, and variables representing domain objects are named `domain_obj`. New code should follow this convention, unless it's in an area that consistently uses `domain` for the object and `domain_name` for the string. There's a `project` attribute attached to requests that's a domain object. In spite of this, don't use `project` in new code. """ _blobdb_type_code = BLOB_CODES.domain name = StringProperty() is_active = BooleanProperty() date_created = DateTimeProperty() default_timezone = StringProperty( default=getattr(settings, "TIME_ZONE", "UTC")) case_sharing = BooleanProperty(default=False) secure_submissions = BooleanProperty(default=False) cloudcare_releases = StringProperty( choices=['stars', 'nostars', 'default'], default='default') organization = StringProperty() hr_name = StringProperty() # the human-readable name for this project project_description = StringProperty() # Brief description of the project creating_user = StringProperty( ) # username of the user who created this domain # domain metadata project_type = StringProperty() # e.g. MCH, HIV customer_type = StringProperty() # plus, full, etc. is_test = StringProperty(choices=["true", "false", "none"], default="none") description = StringProperty() short_description = StringProperty() is_shared = BooleanProperty(default=False) commtrack_enabled = BooleanProperty(default=False) call_center_config = SchemaProperty(CallCenterProperties) restrict_superusers = BooleanProperty(default=False) allow_domain_requests = BooleanProperty(default=False) location_restriction_for_users = BooleanProperty(default=False) usercase_enabled = BooleanProperty(default=False) hipaa_compliant = BooleanProperty(default=False) use_sql_backend = BooleanProperty(default=False) first_domain_for_user = BooleanProperty(default=False) case_display = SchemaProperty(CaseDisplaySettings) # CommConnect settings survey_management_enabled = BooleanProperty(default=False) # Whether or not a case can register via sms sms_case_registration_enabled = BooleanProperty(default=False) # Case type to apply to cases registered via sms sms_case_registration_type = StringProperty() # Owner to apply to cases registered via sms sms_case_registration_owner_id = StringProperty() # Submitting user to apply to cases registered via sms sms_case_registration_user_id = StringProperty() # Whether or not a mobile worker can register via sms sms_mobile_worker_registration_enabled = BooleanProperty(default=False) use_default_sms_response = BooleanProperty(default=False) default_sms_response = StringProperty() chat_message_count_threshold = IntegerProperty() sms_language_fallback = StringProperty() custom_chat_template = StringProperty( ) # See settings.CUSTOM_CHAT_TEMPLATES custom_case_username = StringProperty( ) # Case property to use when showing the case's name in a chat window # If empty, sms can be sent at any time. Otherwise, only send during # these windows of time. SMS_QUEUE_ENABLED must be True in localsettings # for this be considered. restricted_sms_times = SchemaListProperty(DayTimeWindow) # If empty, this is ignored. Otherwise, the framework will make sure # that during these days/times, no automated outbound sms will be sent # to someone if they have sent in an sms within sms_conversation_length # minutes. Outbound sms sent from a user in a chat window, however, will # still be sent. This is meant to prevent chat conversations from being # interrupted by automated sms reminders. # SMS_QUEUE_ENABLED must be True in localsettings for this to be # considered. sms_conversation_times = SchemaListProperty(DayTimeWindow) # In minutes, see above. sms_conversation_length = IntegerProperty(default=10) # Set to True to prevent survey questions and answers form being seen in # SMS chat windows. filter_surveys_from_chat = BooleanProperty(default=False) # The below option only matters if filter_surveys_from_chat = True. # If set to True, invalid survey responses will still be shown in the chat # window, while questions and valid responses will be filtered out. show_invalid_survey_responses_in_chat = BooleanProperty(default=False) # If set to True, if a message is read by anyone it counts as being read by # everyone. Set to False so that a message is only counted as being read # for a user if only that user has read it. count_messages_as_read_by_anyone = BooleanProperty(default=False) enable_registration_welcome_sms_for_case = BooleanProperty(default=False) enable_registration_welcome_sms_for_mobile_worker = BooleanProperty( default=False) sms_survey_date_format = StringProperty() granted_messaging_access = BooleanProperty(default=False) # Allowed outbound SMS per day # If this is None, then the default is applied. See get_daily_outbound_sms_limit() custom_daily_outbound_sms_limit = IntegerProperty() # Allowed number of case updates or closes from automatic update rules in the daily rule run. # If this value is None, the value in settings.MAX_RULE_UPDATES_IN_ONE_RUN is used. auto_case_update_limit = IntegerProperty() # Allowed number of max OData feeds that this domain can create. # If this value is None, the value in settings.DEFAULT_ODATA_FEED_LIMIT is used odata_feed_limit = IntegerProperty() # exchange/domain copying stuff is_snapshot = BooleanProperty(default=False) is_approved = BooleanProperty(default=False) snapshot_time = DateTimeProperty() published = BooleanProperty(default=False) license = StringProperty(choices=LICENSES, default='cc') title = StringProperty() cda = SchemaProperty(LicenseAgreement) multimedia_included = BooleanProperty(default=True) downloads = IntegerProperty( default=0) # number of downloads for this specific snapshot full_downloads = IntegerProperty( default=0) # number of downloads for all snapshots from this domain author = StringProperty() phone_model = StringProperty() attribution_notes = StringProperty() publisher = StringProperty(choices=["organization", "user"], default="user") yt_id = StringProperty() snapshot_head = BooleanProperty(default=False) deployment = SchemaProperty(Deployment) cached_properties = DictProperty() internal = SchemaProperty(InternalProperties) dynamic_reports = SchemaListProperty(DynamicReportSet) # extra user specified properties tags = StringListProperty() area = StringProperty(choices=AREA_CHOICES) sub_area = StringProperty(choices=SUB_AREA_CHOICES) launch_date = DateTimeProperty last_modified = DateTimeProperty(default=datetime(2015, 1, 1)) # when turned on, use SECURE_TIMEOUT for sessions of users who are members of this domain secure_sessions = BooleanProperty(default=False) two_factor_auth = BooleanProperty(default=False) strong_mobile_passwords = BooleanProperty(default=False) requested_report_builder_subscription = StringListProperty() report_whitelist = StringListProperty() # seconds between sending mobile UCRs to users. Can be overridden per user default_mobile_ucr_sync_interval = IntegerProperty() @classmethod def wrap(cls, data): # for domains that still use original_doc should_save = False if 'original_doc' in data: original_doc = data['original_doc'] del data['original_doc'] should_save = True if original_doc: original_doc = Domain.get_by_name(original_doc) data['copy_history'] = [original_doc._id] # for domains that have a public domain license if 'license' in data: if data.get("license", None) == "public": data["license"] = "cc" should_save = True if 'slug' in data and data["slug"]: data["hr_name"] = data["slug"] del data["slug"] if 'is_test' in data and isinstance(data["is_test"], bool): data["is_test"] = "true" if data["is_test"] else "false" should_save = True if 'cloudcare_releases' not in data: data['cloudcare_releases'] = 'nostars' # legacy default setting # Don't actually remove location_types yet. We can migrate fully and # remove this after everything's hunky-dory in production. 2015-03-06 if 'location_types' in data: data['obsolete_location_types'] = data.pop('location_types') if 'granted_messaging_access' not in data: # enable messaging for domains created before this flag was added data['granted_messaging_access'] = True self = super(Domain, cls).wrap(data) if self.deployment is None: self.deployment = Deployment() if should_save: self.save() return self def get_default_timezone(self): """return a timezone object from self.default_timezone""" import pytz return pytz.timezone(self.default_timezone) @staticmethod @quickcache(['name'], timeout=24 * 60 * 60) def is_secure_session_required(name): domain_obj = Domain.get_by_name(name) return domain_obj and domain_obj.secure_sessions @staticmethod @quickcache(['couch_user._id', 'is_active'], timeout=5 * 60, memoize_timeout=10) def active_for_couch_user(couch_user, is_active=True): domain_names = couch_user.get_domains() return Domain.view( "domain/by_status", keys=[[is_active, d] for d in domain_names], reduce=False, include_docs=True, ).all() @staticmethod def active_for_user(user, is_active=True): if isinstance(user, AnonymousUser): return [] from corehq.apps.users.models import CouchUser if isinstance(user, CouchUser): couch_user = user else: couch_user = CouchUser.from_django_user(user) if couch_user: return Domain.active_for_couch_user(couch_user, is_active=is_active) else: return [] def add(self, model_instance, is_active=True): """ Add something to this domain, through the generic relation. Returns the created membership object """ # Add membership info to Couch couch_user = model_instance.get_profile().get_couch_user() couch_user.add_domain_membership(self.name) couch_user.save() def applications(self): return get_brief_apps_in_domain(self.name) def full_applications(self, include_builds=True): from corehq.apps.app_manager.util import get_correct_app_class from corehq.apps.app_manager.models import Application def wrap_application(a): return get_correct_app_class(a['doc']).wrap(a['doc']) if include_builds: startkey = [self.name] endkey = [self.name, {}] else: startkey = [self.name, None] endkey = [self.name, None, {}] return Application.get_db().view('app_manager/applications', startkey=startkey, endkey=endkey, include_docs=True, wrapper=wrap_application).all() @cached_property def versions(self): apps = self.applications() return list(set(a.application_version for a in apps)) @cached_property def has_media(self): from corehq.apps.app_manager.util import is_remote_app for app in self.full_applications(): if not is_remote_app(app) and app.has_media(): return True return False @property def use_cloudcare_releases(self): return self.cloudcare_releases != 'nostars' def all_users(self): from corehq.apps.users.models import CouchUser return CouchUser.by_domain(self.name) def recent_submissions(self): return domain_has_submission_in_last_30_days(self.name) @classmethod @quickcache(['name'], skip_arg='strict', timeout=30 * 60, session_function=icds_conditional_session_key()) def get_by_name(cls, name, strict=False): if not name: # get_by_name should never be called with name as None (or '', etc) # I fixed the code in such a way that if I raise a ValueError # all tests pass and basic pages load, # but in order not to break anything in the wild, # I'm opting to notify by email if/when this happens # but fall back to the previous behavior of returning None if settings.DEBUG: raise ValueError('%r is not a valid domain name' % name) else: _assert = soft_assert(notify_admins=True, exponential_backoff=False) _assert(False, '%r is not a valid domain name' % name) return None def _get_by_name(stale=False): extra_args = {'stale': settings.COUCH_STALE_QUERY} if stale else {} result = cls.view("domain/domains", key=name, reduce=False, include_docs=True, **extra_args).first() if not isinstance(result, Domain): # A stale view may return a result with no doc if the doc has just been deleted. # In this case couchdbkit just returns the raw view result as a dict return None else: return result domain = _get_by_name(stale=(not strict)) if domain is None and not strict: # on the off chance this is a brand new domain, try with strict domain = _get_by_name(stale=False) return domain @classmethod def get_or_create_with_name(cls, name, is_active=False, secure_submissions=True, use_sql_backend=False): result = cls.view("domain/domains", key=name, reduce=False, include_docs=True).first() if result: return result else: new_domain = Domain( name=name, is_active=is_active, date_created=datetime.utcnow(), secure_submissions=secure_submissions, use_sql_backend=use_sql_backend, ) new_domain.save(**get_safe_write_kwargs()) return new_domain @classmethod def generate_name(cls, hr_name, max_length=25): ''' Generate a URL-friendly name based on a given human-readable name. Normalizes given name, then looks for conflicting domains, addressing conflicts by adding "-1", "-2", etc. May return None if it fails to generate a new, unique name. Throws exception if it can't figure out a name, which shouldn't happen unless max_length is absurdly short. ''' from corehq.apps.domain.utils import get_domain_url_slug from corehq.apps.domain.dbaccessors import domain_or_deleted_domain_exists name = get_domain_url_slug(hr_name, max_length=max_length) if not name: raise NameUnavailableException if domain_or_deleted_domain_exists(name): prefix = name while len(prefix): name = next_available_name( prefix, Domain.get_names_by_prefix(prefix + '-')) if domain_or_deleted_domain_exists(name): # should never happen raise NameUnavailableException if len(name) <= max_length: return name prefix = prefix[:-1] raise NameUnavailableException return name @classmethod def get_all(cls, include_docs=True): domains = Domain.view("domain/not_snapshots", include_docs=False).all() if not include_docs: return domains else: return map(cls.wrap, iter_docs(cls.get_db(), [d['id'] for d in domains])) @classmethod def get_all_names(cls): return sorted({d['key'] for d in cls.get_all(include_docs=False)}) @classmethod def get_all_ids(cls): return [d['id'] for d in cls.get_all(include_docs=False)] @classmethod def get_names_by_prefix(cls, prefix): return [ d['key'] for d in Domain.view("domain/domains", startkey=prefix, endkey=prefix + "zzz", reduce=False, include_docs=False).all() ] + [ d['key'] for d in Domain.view("domain/deleted_domains", startkey=prefix, endkey=prefix + "zzz", reduce=False, include_docs=False).all() ] def case_sharing_included(self): return self.case_sharing or reduce(lambda x, y: x or y, [ getattr(app, 'case_sharing', False) for app in self.applications() ], False) def save(self, **params): from corehq.apps.domain.dbaccessors import domain_or_deleted_domain_exists self.last_modified = datetime.utcnow() if not self._rev: if domain_or_deleted_domain_exists(self.name): raise NameUnavailableException(self.name) # mark any new domain as timezone migration complete set_tz_migration_complete(self.name) super(Domain, self).save(**params) from corehq.apps.domain.signals import commcare_domain_post_save results = commcare_domain_post_save.send_robust(sender='domain', domain=self) log_signal_errors(results, "Error occurred during domain post_save (%s)", {'domain': self.name}) def snapshots(self, **view_kwargs): return Domain.view('domain/snapshots', startkey=[self._id, {}], endkey=[self._id], include_docs=True, reduce=False, descending=True, **view_kwargs) def update_deployment(self, **kwargs): self.deployment.update(kwargs) self.save() def update_internal(self, **kwargs): self.internal.update(kwargs) self.save() def display_name(self): if self.is_snapshot: return "Snapshot of %s" % self.copied_from.display_name() return self.hr_name or self.name def long_display_name(self): if self.is_snapshot: return format_html("Snapshot of {}", self.copied_from.display_name()) return self.hr_name or self.name __str__ = long_display_name def get_license_display(self): return LICENSES.get(self.license) def get_license_url(self): return LICENSE_LINKS.get(self.license) def copies(self): return Domain.view('domain/copied_from_snapshot', key=self._id, include_docs=True) def copies_of_parent(self): return Domain.view('domain/copied_from_snapshot', keys=[s._id for s in self.copied_from.snapshots()], include_docs=True) def delete(self, leave_tombstone=False): if not leave_tombstone and not settings.UNIT_TESTING: raise ValueError( 'Cannot delete domain without leaving a tombstone except during testing' ) self._pre_delete() if leave_tombstone: domain = self.get(self._id) if not domain.doc_type.endswith('-Deleted'): domain.doc_type = '{}-Deleted'.format(domain.doc_type) domain.save() else: super().delete() # The save signals can undo effect of clearing the cache within the save # because they query the stale view (but attaches the up to date doc). # This is only a problem on delete/soft-delete, # because these change the presence in the index, not just the doc content. # Since this is rare, I'm opting to just re-clear the cache here # rather than making the signals use a strict lookup or something like that. self.clear_caches() def _pre_delete(self): from corehq.apps.domain.deletion import apply_deletion_operations # delete SQL models first because UCR tables are indexed by configs in couch apply_deletion_operations(self.name) # delete couch docs for db, related_doc_ids in get_all_doc_ids_for_domain_grouped_by_db( self.name): iter_bulk_delete(db, related_doc_ids, chunksize=500) @classmethod def get_module_by_name(cls, domain_name): """ import and return the python module corresponding to domain_name, or None if it doesn't exist. """ module_name = settings.DOMAIN_MODULE_MAP.get(domain_name, domain_name) try: return import_module(module_name) if module_name else None except ImportError: return None @property @memoized def commtrack_settings(self): # this import causes some dependency issues so lives in here from corehq.apps.commtrack.models import CommtrackConfig if self.commtrack_enabled: return CommtrackConfig.for_domain(self.name) else: return None @property def has_custom_logo(self): return self.has_attachment(LOGO_ATTACHMENT) def get_custom_logo(self): if not self.has_custom_logo: return None return (self.fetch_attachment(LOGO_ATTACHMENT), self.blobs[LOGO_ATTACHMENT].content_type) def put_attachment(self, *args, **kw): return super(Domain, self).put_attachment(domain=self.name, *args, **kw) def get_case_display(self, case): """Get the properties display definition for a given case""" return self.case_display.case_details.get(case.type) def get_form_display(self, form): """Get the properties display definition for a given XFormInstance""" return self.case_display.form_details.get(form.xmlns) @property def location_types(self): from corehq.apps.locations.models import LocationType return LocationType.objects.filter(domain=self.name).all() @memoized def has_privilege(self, privilege): from corehq.apps.accounting.utils import domain_has_privilege return domain_has_privilege(self, privilege) @property @memoized def uses_locations(self): from corehq import privileges from corehq.apps.locations.models import LocationType return (self.has_privilege(privileges.LOCATIONS) and (self.commtrack_enabled or LocationType.objects.filter(domain=self.name).exists())) def convert_to_commtrack(self): """ One-stop-shop to make a domain CommTrack """ from corehq.apps.commtrack.util import make_domain_commtrack make_domain_commtrack(self) def clear_caches(self): from .utils import domain_restricts_superusers super(Domain, self).clear_caches() self.get_by_name.clear(self.__class__, self.name) self.is_secure_session_required.clear(self.name) domain_restricts_superusers.clear(self.name) def get_daily_outbound_sms_limit(self): if self.custom_daily_outbound_sms_limit: return self.custom_daily_outbound_sms_limit # https://manage.dimagi.com/default.asp?274299 return 50000
class FixtureTypeField(DocumentSchema): field_name = StringProperty() properties = StringListProperty() is_indexed = BooleanProperty(default=False)
class GroupExportConfiguration(Document): """ An export configuration allows you to setup a collection of exports that all run together. Used by the management command or a scheduled job to run a bunch of exports on a schedule. """ full_exports = SchemaListProperty(ExportConfiguration) custom_export_ids = StringListProperty() def get_custom_exports(self): for custom in list(self.custom_export_ids): custom_export = self._get_custom(custom) if custom_export: yield custom_export def _get_custom(self, custom_id): """ Get a custom export, or delete it's reference if not found """ try: return SavedExportSchema.get(custom_id) except ResourceNotFound: try: self.custom_export_ids.remove(custom_id) self.save() except ValueError: pass @property @memoized def saved_exports(self): return self._saved_exports_from_configs(self.all_configs) def _saved_exports_from_configs(self, configs): exports = SavedBasicExport.view( "couchexport/saved_exports", keys=[json.dumps(config.index) for config in configs], include_docs=True, reduce=False, ).all() export_map = dict((json.dumps(export.configuration.index), export) for export in exports) return [ GroupExportComponent( config, export_map.get(json.dumps(config.index), None), self._id, list(self.all_configs).index(config)) for config in configs ] @property @memoized def all_configs(self): """ Return an iterator of config-like objects that include the main configs + the custom export configs. """ return [full for full in self.full_exports] + \ [custom.to_export_config() for custom in self.get_custom_exports()] @property def all_export_schemas(self): """ Return an iterator of ExportSchema-like objects that include the main configs + the custom export configs. """ for full in self.full_exports: yield DefaultExportSchema(index=full.index, type=full.type) for custom in self.get_custom_exports(): yield custom @property @memoized def all_exports(self): """ Returns an iterator of tuples consisting of the export config and an ExportSchema-like document that can be used to get at the data. """ return list(zip(self.all_configs, self.all_export_schemas))
class ReportConfiguration(UnicodeMixIn, QuickCachedDocumentMixin, Document): """ A report configuration. These map 1:1 with reports that show up in the UI. """ domain = StringProperty(required=True) visible = BooleanProperty(default=True) # config_id of the datasource config_id = StringProperty(required=True) data_source_type = StringProperty( default=DATA_SOURCE_TYPE_STANDARD, choices=[DATA_SOURCE_TYPE_STANDARD, DATA_SOURCE_TYPE_AGGREGATE]) title = StringProperty() description = StringProperty() aggregation_columns = StringListProperty() filters = ListProperty() columns = ListProperty() configured_charts = ListProperty() sort_expression = ListProperty() soft_rollout = DecimalProperty(default=0) # no longer used report_meta = SchemaProperty(ReportMeta) custom_query_provider = StringProperty(required=False) def __unicode__(self): return '{} - {}'.format(self.domain, self.title) def save(self, *args, **kwargs): self.report_meta.last_modified = datetime.utcnow() super(ReportConfiguration, self).save(*args, **kwargs) @property @memoized def filters_without_prefilters(self): return [f for f in self.filters if f['type'] != 'pre'] @property @memoized def prefilters(self): return [f for f in self.filters if f['type'] == 'pre'] @property @memoized def config(self): return get_datasource_config(self.config_id, self.domain, self.data_source_type)[0] @property @memoized def report_columns(self): return [ ReportColumnFactory.from_spec(c, self.is_static) for c in self.columns ] @property @memoized def ui_filters(self): return [ReportFilterFactory.from_spec(f, self) for f in self.filters] @property @memoized def charts(self): return [ChartFactory.from_spec(g._obj) for g in self.configured_charts] @property @memoized def location_column_id(self): cols = [col for col in self.report_columns if col.type == 'location'] if cols: return cols[0].column_id @property def map_config(self): def map_col(column): if column['column_id'] != self.location_column_id: return { 'column_id': column['column_id'], 'label': column['display'] } if self.location_column_id: return { 'location_column_id': self.location_column_id, 'layer_name': { 'XFormInstance': _('Forms'), 'CommCareCase': _('Cases') }.get(self.config.referenced_doc_type, "Layer"), 'columns': [x for x in (map_col(col) for col in self.columns) if x] } @property @memoized def sort_order(self): return [ ReportOrderByFactory.from_spec(e) for e in self.sort_expression ] @property def table_id(self): return self.config.table_id def get_ui_filter(self, filter_slug): for filter in self.ui_filters: if filter.name == filter_slug: return filter return None def get_languages(self): """ Return the languages used in this report's column and filter display properties. Note that only explicitly identified languages are returned. So, if the display properties are all strings, "en" would not be returned. """ langs = set() for item in self.columns + self.filters: if isinstance(item['display'], dict): langs |= set(item['display'].keys()) return langs def validate(self, required=True): from corehq.apps.userreports.reports.data_source import ConfigurableReportDataSource def _check_for_duplicates(supposedly_unique_list, error_msg): # http://stackoverflow.com/questions/9835762/find-and-list-duplicates-in-python-list duplicate_items = set([ item for item in supposedly_unique_list if supposedly_unique_list.count(item) > 1 ]) if len(duplicate_items) > 0: raise BadSpecError( _(error_msg).format(', '.join(sorted(duplicate_items)))) super(ReportConfiguration, self).validate(required) # check duplicates before passing to factory since it chokes on them _check_for_duplicates( [FilterSpec.wrap(f).slug for f in self.filters], 'Filters cannot contain duplicate slugs: {}', ) _check_for_duplicates( [ column_id for c in self.report_columns for column_id in c.get_column_ids() ], 'Columns cannot contain duplicate column_ids: {}', ) # these calls all implicitly do validation ConfigurableReportDataSource.from_spec(self) self.ui_filters self.charts self.sort_order @classmethod @quickcache(['cls.__name__', 'domain']) def by_domain(cls, domain): return get_report_configs_for_domain(domain) @classmethod @quickcache(['cls.__name__', 'domain', 'data_source_id']) def count_by_data_source(cls, domain, data_source_id): return get_number_of_report_configs_by_data_source( domain, data_source_id) def clear_caches(self): super(ReportConfiguration, self).clear_caches() self.by_domain.clear(self.__class__, self.domain) self.count_by_data_source.clear(self.__class__, self.domain, self.config_id) @property def is_static(self): return report_config_id_is_static(self._id)
class FixtureTypeField(DocumentSchema): field_name = StringProperty() properties = StringListProperty()
class AbstractSyncLog(SafeSaveDocument): date = DateTimeProperty() domain = StringProperty() user_id = StringProperty() build_id = StringProperty() # only works with app-aware sync app_id = StringProperty() # only works with app-aware sync previous_log_id = StringProperty() # previous sync log, forming a chain duration = IntegerProperty() # in seconds log_format = StringProperty() # owner_ids_on_phone stores the ids the phone thinks it's the owner of. # This typically includes the user id, # as well as all groups that that user is a member of. owner_ids_on_phone = StringListProperty() # for debugging / logging previous_log_rev = StringProperty( ) # rev of the previous log at the time of creation last_submitted = DateTimeProperty( ) # last time a submission caused this to be modified rev_before_last_submitted = StringProperty( ) # rev when the last submission was saved last_cached = DateTimeProperty( ) # last time this generated a cached response hash_at_last_cached = StringProperty( ) # the state hash of this when it was last cached # save state errors and hashes here had_state_error = BooleanProperty(default=False) error_date = DateTimeProperty() error_hash = StringProperty() cache_payload_paths = DictProperty() last_ucr_sync_times = SchemaListProperty(UCRSyncLog) strict = True # for asserts @classmethod def wrap(cls, data): ret = super(AbstractSyncLog, cls).wrap(data) if hasattr(ret, 'has_assert_errors'): ret.strict = False return ret def save(self): self._synclog_sql = save_synclog_to_sql(self) def delete(self): if getattr(self, '_synclog_sql', None): self._synclog_sql.delete() def case_count(self): """ How many cases are associated with this. Used in reports. """ raise NotImplementedError() def phone_is_holding_case(self, case_id): raise NotImplementedError() def get_footprint_of_cases_on_phone(self): """ Gets the phone's flat list of all case ids on the phone, owned or not owned but relevant. """ raise NotImplementedError() def get_state_hash(self): return CaseStateHash( Checksum(self.get_footprint_of_cases_on_phone()).hexdigest()) def update_phone_lists(self, xform, case_list): """ Given a form an list of touched cases, update this sync log to reflect the updated state on the phone. """ raise NotImplementedError() @classmethod def from_other_format(cls, other_sync_log): """ Convert to an instance of a subclass from another subclass. Subclasses can override this to provide conversion functions. """ raise IncompatibleSyncLogType('Unable to convert from {} to {}'.format( type(other_sync_log), cls, )) # anything prefixed with 'tests_only' is only used in tests def tests_only_get_cases_on_phone(self): raise NotImplementedError() def test_only_clear_cases_on_phone(self): raise NotImplementedError() def test_only_get_dependent_cases_on_phone(self): raise NotImplementedError()
class ReportConfiguration(UnicodeMixIn, CachedCouchDocumentMixin, Document): """ A report configuration. These map 1:1 with reports that show up in the UI. """ domain = StringProperty(required=True) visible = BooleanProperty(default=True) config_id = StringProperty(required=True) title = StringProperty() description = StringProperty() aggregation_columns = StringListProperty() filters = ListProperty() columns = ListProperty() configured_charts = ListProperty() sort_expression = ListProperty() report_meta = SchemaProperty(ReportMeta) def __unicode__(self): return u'{} - {}'.format(self.domain, self.title) @property @memoized def config(self): try: return DataSourceConfiguration.get(self.config_id) except ResourceNotFound: raise BadSpecError(_('The data source referenced by this report could not be found.')) @property @memoized def report_columns(self): return [ReportColumnFactory.from_spec(c) for c in self.columns] @property @memoized def ui_filters(self): return [ReportFilterFactory.from_spec(f) for f in self.filters] @property @memoized def charts(self): return [ChartFactory.from_spec(g._obj) for g in self.configured_charts] @property @memoized def sort_order(self): return [ReportOrderByFactory.from_spec(e) for e in self.sort_expression] @property def table_id(self): return self.config.table_id def get_ui_filter(self, filter_slug): for filter in self.ui_filters: if filter.name == filter_slug: return filter return None def get_languages(self): """ Return the languages used in this report's column and filter display properties. Note that only explicitly identified languages are returned. So, if the display properties are all strings, "en" would not be returned. """ langs = set() for item in self.columns + self.filters: if isinstance(item['display'], dict): langs |= set(item['display'].keys()) return langs def validate(self, required=True): def _check_for_duplicates(supposedly_unique_list, error_msg): # http://stackoverflow.com/questions/9835762/find-and-list-duplicates-in-python-list duplicate_items = set( [item for item in supposedly_unique_list if supposedly_unique_list.count(item) > 1] ) if len(duplicate_items) > 0: raise BadSpecError( _(error_msg).format(', '.join(sorted(duplicate_items))) ) super(ReportConfiguration, self).validate(required) # check duplicates before passing to factory since it chokes on them _check_for_duplicates( [FilterSpec.wrap(f).slug for f in self.filters], 'Filters cannot contain duplicate slugs: {}', ) _check_for_duplicates( [column_id for c in self.report_columns for column_id in c.get_column_ids()], 'Columns cannot contain duplicate column_ids: {}', ) # these calls all implicitly do validation ReportFactory.from_spec(self) self.ui_filters self.charts self.sort_order @classmethod def by_domain(cls, domain): return get_report_configs_for_domain(domain) @classmethod def all(cls): return get_all_report_configs()