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") services = StringProperty(choices=["", "basic", "plus", "full", "custom"], default="") initiative = StringListProperty() workshop_region = StringProperty() project_state = StringProperty( choices=["", "POC", "transition", "at-scale"], default="") self_started = BooleanProperty() area = StringProperty() sub_area = StringProperty() using_adm = BooleanProperty() using_call_center = BooleanProperty() custom_eula = BooleanProperty() can_use_data = BooleanProperty() notes = StringProperty() organization_name = StringProperty() platform = StringListProperty() project_manager = StringProperty() phone_model = StringProperty() goal_time_period = IntegerProperty() goal_followup_rate = DecimalProperty()
class MVPChildCasesByAgeIndicatorDefinition(MVPActiveCasesIndicatorDefinition): """ Returns the number of child cases that were active within the datespan provided and have a date of birth that is less than the age provided by days in age. """ max_age_in_days = IntegerProperty() min_age_in_days = IntegerProperty(default=0) show_active_only = BooleanProperty(default=True) is_dob_in_datespan = BooleanProperty(default=False) _admin_crud_class = MVPChildCasesByAgeCRUDManager def get_value_by_status(self, status, user_id, datespan): cases = self._get_cases_by_status(status, user_id, datespan) return self._filter_by_age(cases, datespan) def _filter_by_age(self, results, datespan): valid_case_ids = [] datespan = self._apply_datespan_shifts(datespan) for item in results: if item.get('value'): try: date_of_birth = dateutil.parser.parse(item['value']) valid_id = False if self.is_dob_in_datespan: if datespan.startdate <= date_of_birth <= datespan.enddate: valid_id = True else: td = datespan.enddate - date_of_birth if self.min_age_in_days <= td.days < self.max_age_in_days: valid_id = True if valid_id: valid_case_ids.append(item['id']) except Exception: logging.error("date of birth could not be parsed") return valid_case_ids def get_value(self, user_ids, datespan=None): if self.show_active_only: return super(MVPChildCasesByAgeIndicatorDefinition, self).get_value(user_ids, datespan=datespan) else: results = self.get_raw_results(user_ids, datespan) all_cases = self._filter_by_age(results, datespan) return len(all_cases) @classmethod def get_nice_name(cls): return "MVP Child Cases"
class LicenseAgreement(DocumentSchema): signed = BooleanProperty(default=False) type = StringProperty() date = DateTimeProperty() user_id = StringProperty() user_ip = StringProperty() version = StringProperty()
class ComputedDocumentMixin(DocumentSchema): """ Use this mixin for things like CommCareCase or XFormInstance documents that take advantage of indicator definitions. computed_ is namespaced and may look like the following for indicators: computed_: { mvp_indicators: { indicator_slug: { version: 1, value: "foo" } } } """ computed_ = DictProperty() computed_modified_on_ = DateTimeProperty() # a flag for the indicator pillows so that there aren't any Document Update Conflicts initial_processing_complete = BooleanProperty(default=False) def update_indicator(self, indicator_def, save_on_update=True): existing_indicators = self.computed_.get(indicator_def.namespace, {}) updated_indicators, is_update = indicator_def.update_computed_namespace( existing_indicators, self) if is_update: self.computed_[indicator_def.namespace] = updated_indicators self.computed_modified_on_ = datetime.datetime.utcnow() if save_on_update: self.save() return is_update
class Deployment(DocumentSchema, UpdatableSchema): date = DateTimeProperty() city = StringProperty() country = StringProperty() region = StringProperty() # e.g. US, LAC, SA, Sub-saharn Africa, East Africa, West Africa, Southeast Asia) description = StringProperty() public = BooleanProperty(default=False)
class ExportColumn(DocumentSchema): """ A column configuration, for export """ index = StringProperty() display = StringProperty() # signature: transform(val, doc) -> val transform = SerializableFunctionProperty(default=None) tag = StringProperty() is_sensitive = BooleanProperty(default=False) show = BooleanProperty(default=False) @classmethod def wrap(self, data): if 'is_sensitive' not in data and data.get('transform', None): data['is_sensitive'] = True if 'doc_type' in data and \ self.__name__ == ExportColumn.__name__ and \ self.__name__ != data['doc_type']: if data['doc_type'] in column_types: return column_types[data['doc_type']].wrap(data) else: raise ResourceNotFound('Unknown column type: %s', data) else: return super(ExportColumn, self).wrap(data) def get_display(self): return u'{primary}{extra}'.format( primary=self.display, extra=" [sensitive]" if self.is_sensitive else '') def to_config_format(self, selected=True): return { "index": self.index, "display": self.display, "transform": self.transform.dumps() if self.transform else None, "is_sensitive": self.is_sensitive, "selected": selected, "tag": self.tag, "show": self.show, }
class Notification(Document): """ For handling persistent notifications that only disappear when the user explicitly dismisses them. Example Usage: class ExampleNotification(Notification): doc_type = 'ExampleNotification' def template(self): return 'example_notification.html' def example_page(request): ExampleNotification.display_if_needed(messages, request) """ dismissed = BooleanProperty(default=False) user = StringProperty() base_doc = 'Notification' doc_type = 'Notification' def template(self): raise NotImplementedError @classmethod # @memoized todo: figure out how to reset cache of class methods def get_notification(cls, username): notification = cls.view("announcements/notifications", reduce=False, startkey=[cls._doc_type, username], endkey=[cls._doc_type, username, {}], include_docs=True, ).one() if not notification: notification = cls(user=username) notification.save() return notification @classmethod def unseen_notification(cls, username): notification = cls.get_notification(username) return notification if not notification.dismissed else None def render_notice(self, ctxt=None): ctxt = ctxt or {} ctxt.update({"note": self, "notification_template": self.template()}) return render_to_string('announcements/partials/notification_wrapper.html', ctxt) @classmethod def display_if_needed(cls, messages, request, ctxt=None): note = cls.unseen_notification(request.couch_user.username) if note: ctxt = ctxt or {} messages.info(request, note.render_notice(ctxt=ctxt), extra_tags="html")
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=["", "standard", "plus", "advanced"], default="") services = StringProperty(choices=["", "basic", "plus", "full", "custom"], default="") initiative = StringListProperty() project_state = StringProperty(choices=["", "POC", "transition", "at-scale"], default="") self_started = BooleanProperty() area = StringProperty() sub_area = StringProperty() using_adm = BooleanProperty() using_call_center = BooleanProperty() custom_eula = BooleanProperty() can_use_data = BooleanProperty() notes = StringProperty() organization_name = StringProperty() platform = StringListProperty()
class HQBillingDomainMixin(DocumentSchema): """ This contains all the attributes required to bill a client for CommCare HQ services. """ billing_address = SchemaProperty(HQBillingAddress) billing_number = StringProperty() currency_code = StringProperty(default=settings.DEFAULT_CURRENCY) # used to bill client is_sms_billable = BooleanProperty() billable_client = StringProperty() def update_billing_info(self, **kwargs): self.billing_number = kwargs.get('phone_number','') self.billing_address.update_billing_address(**kwargs) self.currency_code = kwargs.get('currency_code', settings.DEFAULT_CURRENCY)
class DomainMigrations(DocumentSchema): has_migrated_permissions = BooleanProperty(default=False) def apply(self, domain): if not self.has_migrated_permissions: logging.info("Applying permissions migration to domain %s" % domain.name) from corehq.apps.users.models import UserRole, WebUser UserRole.init_domain_with_presets(domain.name) for web_user in WebUser.by_domain(domain.name): try: web_user.save() except ResourceConflict: # web_user has already been saved by another thread in the last few seconds pass self.has_migrated_permissions = True domain.save()
class XFormsSession(Document): """ Keeps information about an SMS XForm session. """ # generic properties connection_id = StringProperty() session_id = StringProperty() form_xmlns = StringProperty() start_time = DateTimeProperty() modified_time = DateTimeProperty() end_time = DateTimeProperty() completed = BooleanProperty() # HQ specific properties domain = StringProperty() user_id = StringProperty() app_id = StringProperty() submission_id = StringProperty() survey_incentive = StringProperty() session_type = StringProperty(choices=XFORMS_SESSION_TYPES, default=XFORMS_SESSION_SMS) def save(self, *args, **kwargs): if is_bigcouch() and "w" not in kwargs: # Force a write to all nodes before returning kwargs["w"] = bigcouch_quorum_count() return super(XFormsSession, self).save(*args, **kwargs) def __unicode__(self): return 'Form %(form)s in domain %(domain)s. Last modified: %(mod)s' % \ {"form": self.form_xmlns, "domain": self.domain, "mod": self.modified_time} def end(self, completed): """ Marks this as ended (by setting end time). """ self.completed = completed self.modified_time = self.end_time = datetime.utcnow() @classmethod def latest_by_session_id(cls, id): return XFormsSession.view("smsforms/sessions_by_touchforms_id", startkey=[id], endkey=[id, {}], include_docs=True).one()
class RegistrationRequest(Document): tos_confirmed = BooleanProperty(default=False) request_time = DateTimeProperty() request_ip = StringProperty() activation_guid = StringProperty() confirm_time = DateTimeProperty() confirm_ip = StringProperty() domain = StringProperty() new_user_username = StringProperty() requesting_user_username = StringProperty() @property @memoized def project(self): return Domain.get_by_name(self.domain) @classmethod def get_by_guid(cls, guid): result = cls.view("registration/requests_by_guid", key=guid, reduce=False, include_docs=True).first() return result @classmethod def get_requests_today(cls): today = datetime.datetime.utcnow() yesterday = today - datetime.timedelta(1) result = cls.view("registration/requests_by_time", startkey=yesterday.isoformat(), endkey=today.isoformat(), reduce=True).all() if not result: return 0 return result[0]['value'] @classmethod def get_request_for_username(cls, username): result = cls.view("registration/requests_by_username", key=username, reduce=False, include_docs=True).first() return result
class ILSGatewayConfig(Document): enabled = BooleanProperty(default=False) domain = StringProperty() url = StringProperty(default="http://ilsgateway.com/api/v0_1") username = StringProperty() password = StringProperty() @classmethod def for_domain(cls, name): try: mapping = DocDomainMapping.objects.get(domain_name=name, doc_type='ILSGatewayConfig') return cls.get(docid=mapping.doc_id) except DocDomainMapping.DoesNotExist: return None @classmethod def get_all_configs(cls): mappings = DocDomainMapping.objects.filter(doc_type='ILSGatewayConfig') configs = [cls.get(docid=mapping.doc_id) for mapping in mappings] return configs @classmethod def get_all_enabled_domains(cls): configs = cls.get_all_configs() return [c.domain for c in filter(lambda config: config.enabled, configs)] @property def is_configured(self): return True if self.enabled and self.url and self.password and self.username else False def save(self, **params): super(ILSGatewayConfig, self).save(**params) try: DocDomainMapping.objects.get(doc_id=self._id, domain_name=self.domain, doc_type="ILSGatewayConfig") except DocDomainMapping.DoesNotExist: DocDomainMapping.objects.create(doc_id=self._id, domain_name=self.domain, doc_type='ILSGatewayConfig')
class CDotWeeklySchedule(Document): """Weekly schedule where each day has a username""" schedule_id = StringProperty(default=make_uuid) sunday = StringProperty() monday = StringProperty() tuesday = StringProperty() wednesday = StringProperty() thursday = StringProperty() friday = StringProperty() saturday = StringProperty() comment = StringProperty() deprecated = BooleanProperty(default=False) started = DateTimeProperty(default=datetime.utcnow, required=True) ended = DateTimeProperty() created_by = StringProperty() #userid edited_by = StringProperty() #userid def weekly_arr(self): return [ "Sun: %s" % self.sunday, "Mon: %s" % self.monday, "Tue: %s" % self.tuesday, "Wed: %s" % self.wednesday, "Thu: %s" % self.thursday, "Fri: %s" % self.friday, "Sat: %s" % self.saturday, "Deprecated: %s" % self.deprecated, "Started: %s" % self.started, "Ended: %s" % self.ended, ] class Meta: app_label='pact'
class HQAnnouncement(Document, AdminCRUDDocumentMixin): """ For global, site-wide HQ Announcements. """ title = StringProperty() summary = StringProperty() highlighted_selectors = StringListProperty() date_created = DateTimeProperty() valid_until = DateTimeProperty() show_to_new_users = BooleanProperty(default=False) base_doc = "HQAnnouncement" _admin_crud_class = HQAnnouncementCRUDManager @property def as_html(self): return render_to_string("announcements/partials/base_announcement.html", { 'title': self.title, 'content': self.summary, 'announcement_id': self._id, })
class ReportNotification(CachedCouchDocumentMixin, Document): domain = StringProperty() owner_id = StringProperty() recipient_emails = StringListProperty() config_ids = StringListProperty() send_to_owner = BooleanProperty() attach_excel = BooleanProperty() # language is only used if some of the config_ids refer to UCRs. language = StringProperty() email_subject = StringProperty(default=DEFAULT_REPORT_NOTIF_SUBJECT) hour = IntegerProperty(default=8) minute = IntegerProperty(default=0) day = IntegerProperty(default=1) interval = StringProperty(choices=["daily", "weekly", "monthly"]) uuid = StringProperty() start_date = DateProperty(default=None) @property def is_editable(self): try: self.report_slug return False except AttributeError: return True @classmethod def by_domain_and_owner(cls, domain, owner_id, stale=True, **kwargs): if stale: kwargs['stale'] = settings.COUCH_STALE_QUERY key = [domain, owner_id] db = cls.get_db() result = cache_core.cached_view(db, "reportconfig/user_notifications", reduce=False, include_docs=True, startkey=key, endkey=key + [{}], wrapper=cls.wrap, **kwargs) return result @property @memoized def all_recipient_emails(self): # handle old documents if not self.owner_id: return frozenset([self.owner.get_email()]) emails = frozenset(self.recipient_emails) if self.send_to_owner and self.owner_email: emails |= {self.owner_email} return emails @property @memoized def owner_email(self): if self.owner.is_web_user(): return self.owner.username email = self.owner.get_email() try: validate_email(email) return email except Exception: pass @property @memoized def owner(self): id = self.owner_id return CouchUser.get_by_user_id(id) @property @memoized def configs(self): """ Access the notification's associated configs as a list, transparently returning an appropriate dummy for old notifications which have `report_slug` instead of `config_ids`. """ if self.config_ids: configs = [] for config_doc in iter_docs(ReportConfig.get_db(), self.config_ids): config = ReportConfig.wrap(config_doc) if not hasattr(config, 'deleted'): configs.append(config) def _sort_key(config_id): if config_id in self.config_ids: return self.config_ids.index(config_id) else: return len(self.config_ids) configs = sorted(configs, key=_sort_key) elif self.report_slug == 'admin_domains': raise UnsupportedScheduledReportError( "admin_domains is no longer " "supported as a schedulable report for the time being") else: # create a new ReportConfig object, useful for its methods and # calculated properties, but don't save it class ReadonlyReportConfig(ReportConfig): def save(self, *args, **kwargs): pass config = ReadonlyReportConfig() object.__setattr__(config, '_id', 'dummy') config.report_type = ProjectReportDispatcher.prefix config.report_slug = self.report_slug config.domain = self.domain config.owner_id = self.owner_id configs = [config] return tuple(configs) @property def day_name(self): if self.interval == 'weekly': return calendar.day_name[self.day] return { "daily": _("Every day"), "monthly": _("Day %s of every month" % self.day), }[self.interval] @classmethod def day_choices(cls): """Tuples for day of week number and human-readable day of week""" return tuple([(val, calendar.day_name[val]) for val in range(7)]) @classmethod def hour_choices(cls): """Tuples for hour number and human-readable hour""" return tuple([(val, "%s:00" % val) for val in range(24)]) @property @memoized def recipients_by_language(self): user_languages = { user['username']: user['language'] for user in get_user_docs_by_username(self.all_recipient_emails) if 'username' in user and 'language' in user } fallback_language = user_languages.get(self.owner_email, 'en') recipients = defaultdict(list) for email in self.all_recipient_emails: language = user_languages.get(email, fallback_language) recipients[language].append(email) return immutabledict(recipients) def get_secret(self, email): uuid = self._get_or_create_uuid() return hashlib.sha1((uuid + email).encode('utf-8')).hexdigest()[:20] def send(self): # Scenario: user has been removed from the domain that they # have scheduled reports for. Delete this scheduled report if not self.owner.is_member_of(self.domain): self.delete() return if self.owner.is_deleted(): self.delete() return if self.recipients_by_language: for language, emails in self.recipients_by_language.items(): self._get_and_send_report(language, emails) def _get_or_create_uuid(self): if not self.uuid: self.uuid = uuid.uuid4().hex self.save() return self.uuid def _get_and_send_report(self, language, emails): from corehq.apps.reports.views import get_scheduled_report_response, render_full_report_notification with localize(language): title = (_(DEFAULT_REPORT_NOTIF_SUBJECT) if self.email_subject == DEFAULT_REPORT_NOTIF_SUBJECT else self.email_subject) attach_excel = getattr(self, 'attach_excel', False) content, excel_files = get_scheduled_report_response( self.owner, self.domain, self._id, attach_excel=attach_excel, send_only_active=True) # Will be False if ALL the ReportConfigs in the ReportNotification # have a start_date in the future. if content is False: return for email in emails: body = render_full_report_notification(None, content, email, self).content send_html_email_async.delay( title, email, body, email_from=settings.DEFAULT_FROM_EMAIL, file_attachments=excel_files) def remove_recipient(self, email): try: if email == self.owner.get_email(): self.send_to_owner = False self.recipient_emails.remove(email) except ValueError: pass def update_attributes(self, items): for k, v in items: if k == 'start_date': self.verify_start_date(v) self.__setattr__(k, v) def verify_start_date(self, start_date): if start_date != self.start_date and start_date < datetime.today( ).date(): raise ValidationError( "You can not specify a start date in the past.")
class FixtureDataType(Document): domain = StringProperty() is_global = BooleanProperty(default=False) tag = StringProperty() fields = SchemaListProperty(FixtureTypeField) item_attributes = StringListProperty() @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): num_fixtures = FixtureDataType.get_db().view( 'fixtures/data_types_by_domain', reduce=True, key=domain, ).first() return num_fixtures['value'] if num_fixtures is not None else 0 @classmethod def by_domain(cls, domain): return cls.view('fixtures/data_types_by_domain', key=domain, reduce=False, include_docs=True, descending=True) @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 CObservation(Document): doc_id = StringProperty() patient = StringProperty() #case id pact_id = StringProperty() #patient pact id provider = StringProperty() encounter_date = DateTimeProperty() anchor_date = DateTimeProperty() observed_date = DateTimeProperty() submitted_date = DateTimeProperty() created_date = DateTimeProperty() is_art = BooleanProperty() dose_number = IntegerProperty() total_doses = IntegerProperty() adherence = StringProperty() # DOT_OBSERVATION_ types method = StringProperty() is_reconciliation = BooleanProperty(default=False) day_index = IntegerProperty() day_note = StringProperty( ) #if there's something for that particular day, then it'll be here day_slot = IntegerProperty( ) #new addition, if there's a slot for the day label, then retain it note = StringProperty( ) #this is for the overall note for that submission, will exist on the anchor date @classmethod def wrap(cls, obj): ints = ['dose_number', 'total_doses', 'day_index', 'day_slot'] for prop_name in ints: val = obj.get(prop_name) if val and isinstance(val, basestring): obj[prop_name] = int(val) return super(CObservation, cls).wrap(obj) @property def obs_score(self): """Gets the relative score of the observation. """ if self.method == "direct": return 3 if self.method == "pillbox": return 2 if self.method == "self": return 1 @property def adinfo(self): """helper function to concatenate adherence and method to check for conflicts""" return ((self.is_art, self.dose_number, self.total_doses), "%s" % (self.adherence)) def get_time_label(self): """ old style way returns an English time label out of 'Dose', 'Morning', 'Noon', 'Evening', 'Bedtime' """ return TIME_LABEL_LOOKUP[self.total_doses][self.dose_number] @classmethod def get_time_labels(cls, total_doses): return TIME_LABEL_LOOKUP[total_doses] class Meta: app_label = 'pact' def __unicode__(self): return "Obs %s [%s] %d/%d" % (self.observed_date.strftime("%Y-%m-%d"), "ART" if self.is_art else "NonART", self.dose_number + 1, self.total_doses) def __str__(self): return "Obs %s [%s] %d/%d" % (self.observed_date.strftime("%Y-%m-%d"), "ART" if self.is_art else "NonART", self.dose_number + 1, self.total_doses) def __repr__(self): return simplejson.dumps(self.to_json(), indent=4)
class Notification(Document): """ For handling persistent notifications that only disappear when the user explicitly dismisses them. Example Usage: class ExampleNotification(Notification): doc_type = 'ExampleNotification' def template(self): return 'example_notification.html' def example_page(request): ExampleNotification.display_if_needed(messages, request) """ dismissed = BooleanProperty(default=False) user = StringProperty() base_doc = 'Notification' doc_type = 'Notification' def template(self): raise NotImplementedError @classmethod def get_notification(cls, username): notifications = cache_core.cached_view( cls.get_db(), "announcements/notifications", reduce=False, startkey=[cls._doc_type, username], endkey=[cls._doc_type, username, {}], include_docs=True, wrapper=cls.wrap) try: if len(notifications) > 1: for dup_notification in notifications[1:]: # delete the duplicates dup_notification.base_doc += DELETED_SUFFIX dup_notification.save() notification = notifications[0] except IndexError: notification = None if not notification: notification = cls(user=username) notification.save() return notification @classmethod def unseen_notification(cls, username): notification = cls.get_notification(username) return notification if not notification.dismissed else None def render_notice(self, ctxt=None): ctxt = ctxt or {} ctxt.update({"note": self, "notification_template": self.template()}) return render_to_string('announcements/partials/notification_wrapper.html', ctxt) @classmethod def display_if_needed(cls, messages, request, ctxt=None): note = cls.unseen_notification(request.couch_user.username) if note: ctxt = ctxt or {} messages.info(request, note.render_notice(ctxt=ctxt), extra_tags="html")
class Domain(Document, HQBillingDomainMixin, 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() is_public = BooleanProperty(default=False) date_created = DateTimeProperty() default_timezone = StringProperty( default=getattr(settings, "TIME_ZONE", "UTC")) case_sharing = BooleanProperty(default=False) secure_submissions = BooleanProperty(default=False) ota_restore_caching = BooleanProperty(default=False) cloudcare_releases = StringProperty( choices=['stars', 'nostars', 'default'], default='default') organization = StringProperty() hr_name = StringProperty( ) # the human-readable name for this project within an organization 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) location_restriction_for_users = BooleanProperty(default=True) case_display = SchemaProperty(CaseDisplaySettings) # CommConnect settings commconnect_enabled = BooleanProperty(default=False) survey_management_enabled = BooleanProperty(default=False) sms_case_registration_enabled = BooleanProperty( default=False) # Whether or not a case can register via sms sms_case_registration_type = StringProperty( ) # Case type to apply to cases registered via sms sms_case_registration_owner_id = StringProperty( ) # Owner to apply to cases registered via sms sms_case_registration_user_id = StringProperty( ) # Submitting user to apply to cases registered via sms sms_mobile_worker_registration_enabled = BooleanProperty( default=False) # Whether or not a mobile worker can register via sms default_sms_backend_id = StringProperty() 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=False) # 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() deployment = SchemaProperty(Deployment) image_path = StringProperty() image_type = StringProperty() migrations = SchemaProperty(DomainMigrations) 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', 'country', 'region', 'customer_type') @property def domain_type(self): """ The primary type of this domain. Used to determine site-specific branding. """ if self.commtrack_enabled: return 'commtrack' else: return 'commcare' @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 self = super(Domain, cls).wrap(data) if self.deployment is None: self.deployment = Deployment() if self.get_id: self.apply_migrations() if should_save: self.save() return self @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: domain_names = couch_user.get_domains() return cache_core.cached_view(Domain.get_db(), "domain/by_status", keys=[[is_active, d] for d in domain_names], reduce=False, include_docs=True, wrapper=Domain.wrap) else: return [] @classmethod def field_by_prefix(cls, field, prefix='', is_approved=True): # unichr(0xfff8) is something close to the highest character available res = cls.view( "domain/fields_by_prefix", group=True, startkey=[field, is_approved, prefix], endkey=[field, is_approved, "%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] @classmethod def get_by_field(cls, field, value, is_approved=True): return cls.view('domain/fields_by_prefix', key=[field, is_approved, value], reduce=False, include_docs=True).all() def apply_migrations(self): self.migrations.apply(self) @staticmethod def all_for_user(user): if not hasattr(user, 'get_profile'): # this had better be an anonymous user return [] from corehq.apps.users.models import CouchUser couch_user = CouchUser.from_django_user(user) if couch_user: domain_names = couch_user.get_domains() return Domain.view("domain/domains", keys=domain_names, reduce=False, include_docs=True).all() 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): from corehq.apps.app_manager.models import ApplicationBase return ApplicationBase.view('app_manager/applications_brief', startkey=[self.name], endkey=[self.name, {}]).all() 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 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 has_shared_media(self): return False def recent_submissions(self): from corehq.apps.reports.util import make_form_couch_key key = make_form_couch_key(self.name) res = get_db().view('reports_forms/all_forms', startkey=key + [{}], endkey=key, descending=True, reduce=False, include_docs=False, limit=1).all() if len(res ) > 0: # if there have been any submissions in the past 30 days return (datetime.now() <= datetime.strptime(res[0]['key'][2], "%Y-%m-%dT%H:%M:%SZ") + timedelta(days=30)) else: return False @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 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 try: raise ValueError('%r is not a valid domain name' % name) except ValueError: if settings.DEBUG: raise else: notify_exception(None, '%r is not a valid domain name' % name) return None cache_key = _domain_cache_key(name) MISSING = object() res = cache.get(cache_key, MISSING) if res != MISSING: return res else: domain = cls._get_by_name(name, strict) # 30 mins, so any unforeseen invalidation bugs aren't too bad. cache.set(cache_key, domain, 30 * 60) return domain @classmethod def _get_by_name(cls, name, strict=False): extra_args = { 'stale': settings.COUCH_STALE_QUERY } if not strict else {} db = cls.get_db() res = cache_core.cached_view(db, "domain/domains", key=name, reduce=False, include_docs=True, wrapper=cls.wrap, force_invalidate=strict, **extra_args) if len(res) > 0: result = res[0] else: result = None if result is None and not strict: # on the off chance this is a brand new domain, try with strict return cls.get_by_name(name, strict=True) return result @classmethod def get_by_organization(cls, organization): result = cache_core.cached_view(cls.get_db(), "domain/by_organization", startkey=[organization], endkey=[organization, {}], reduce=False, include_docs=True, wrapper=cls.wrap) from corehq.apps.accounting.utils import domain_has_privilege from corehq import privileges result = filter( lambda x: domain_has_privilege(x.name, privileges. CROSS_PROJECT_REPORTS), result) return result @classmethod def get_by_organization_and_hrname(cls, organization, hr_name): result = cls.view("domain/by_organization", key=[organization, hr_name], reduce=False, include_docs=True) return result @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.migrations = DomainMigrations( has_migrated_permissions=True) new_domain.save(**get_safe_write_kwargs()) return new_domain def password_format(self): """ This was a performance hit, so for now we'll just return 'a' no matter what If a single application is alphanumeric, return alphanumeric; otherwise, return numeric """ return 'a' @classmethod def get_all(cls, include_docs=True): # todo: this should use iter_docs return Domain.view("domain/not_snapshots", include_docs=include_docs).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): super(Domain, self).save(**params) cache.delete(_domain_cache_key(self.name)) 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, user=None, ignore=None): from corehq.apps.app_manager.models import get_app from corehq.apps.reminders.models import CaseReminderHandler ignore = ignore if ignore is not None else [] if new_domain_name is not None and Domain.get_by_name(new_domain_name): return None db = get_db() new_id = db.copy_doc(self.get_id)['id'] if new_domain_name is None: new_domain_name = new_id new_domain = Domain.get(new_id) new_domain.name = new_domain_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) new_comps = {} # a mapping of component's id to it's copy for res in db.view('domain/related_to_domain', key=[self.name, True]): if not self.is_snapshot and res['value']['doc_type'] in ( 'Application', 'RemoteApp'): app = get_app(self.name, res['value']['_id']).get_latest_saved() if app: comp = self.copy_component(app.doc_type, app._id, new_domain_name, user=user) else: comp = self.copy_component(res['value']['doc_type'], res['value']['_id'], new_domain_name, user=user) elif res['value']['doc_type'] not in ignore: comp = self.copy_component(res['value']['doc_type'], res['value']['_id'], new_domain_name, user=user) else: comp = None if comp: new_comps[res['value']['_id']] = comp new_domain.save() 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_comps[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 'CaseReminderHandler' not in ignore: 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 str_to_cls = { 'UserRole': UserRole, 'CaseReminderHandler': CaseReminderHandler, } db = get_db() if doc_type in ('Application', 'RemoteApp'): new_doc = import_app(id, new_domain_name) new_doc.copy_history.append(id) else: cls = str_to_cls[doc_type] 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 self.is_snapshot and doc_type == 'Application': new_doc.prepare_multimedia_for_exchange() new_doc.save() return new_doc def save_snapshot(self, ignore=None): if self.is_snapshot: return self else: copy = self.save_copy(ignore=ignore) if copy is None: return None copy.is_snapshot = True copy.snapshot_time = datetime.now() del copy.deployment copy.save() return copy def from_snapshot(self): return not self.is_snapshot and self.original_doc is not None def snapshots(self): return Domain.view('domain/snapshots', startkey=[self._id, {}], endkey=[self._id], include_docs=True, reduce=False, descending=True) @memoized def published_snapshot(self): snapshots = self.snapshots().all() for snapshot in snapshots: if snapshot.published: return snapshot return None @classmethod def published_snapshots(cls, include_unapproved=False, page=None, per_page=10): skip = None limit = None if page: skip = (page - 1) * per_page limit = per_page if include_unapproved: return cls.view('domain/published_snapshots', startkey=[False, {}], include_docs=True, descending=True, limit=limit, skip=skip) else: return cls.view('domain/published_snapshots', endkey=[True], include_docs=True, descending=True, limit=limit, skip=skip) @classmethod def snapshot_search(cls, query, page=None, per_page=10): skip = None limit = None if page: skip = (page - 1) * per_page limit = per_page results = get_db().search( 'domain/snapshot_search', q=json.dumps(query), limit=limit, skip=skip, #stale='ok', ) return map(cls.get, [r['id'] for r in results]), results.total_rows @memoized def get_organization(self): from corehq.apps.orgs.models import Organization return Organization.get_by_name(self.organization) @memoized def organization_title(self): if self.organization: return self.get_organization().title else: return '' 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() if self.hr_name and self.organization: return self.hr_name else: return self.name def long_display_name(self): if self.is_snapshot: return format_html("Snapshot of {0} > {1}", self.get_organization().title, self.copied_from.display_name()) if self.organization: return format_html('{0} > {1}', self.get_organization().title, self.hr_name or self.name) else: return self.name __str__ = long_display_name def get_license_display(self): return LICENSES.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): # delete all associated objects db = get_db() related_docs = db.view('domain/related_to_domain', startkey=[self.name], endkey=[self.name, {}], include_docs=True) for doc in related_docs: db.delete_doc(doc['doc']) super(Domain, self).delete() 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 popular_sort(cls, domains): sorted_list = [] MIN_REVIEWS = 1.0 domains = [(domain, Review.get_average_rating_by_app(domain.copied_from._id), Review.get_num_ratings_by_app(domain.copied_from._id)) for domain in domains] domains = [(domain, avg or 0.0, num or 0) for domain, avg, num in domains] total_average_sum = sum(avg for domain, avg, num in domains) total_average_count = len(domains) if not total_average_count: return [] total_average = (total_average_sum / total_average_count) for domain, average_rating, num_ratings in domains: if num_ratings == 0: sorted_list.append((0.0, domain)) else: weighted_rating = ( (num_ratings / (num_ratings + MIN_REVIEWS)) * average_rating + (MIN_REVIEWS / (num_ratings + MIN_REVIEWS)) * total_average) sorted_list.append((weighted_rating, domain)) sorted_list = [ domain for weighted_rating, domain in sorted( sorted_list, key=lambda domain: domain[0], reverse=True) ] return sorted_list @classmethod def hit_sort(cls, domains): domains = list(domains) domains = sorted(domains, key=lambda domain: domain.download_count, reverse=True) return domains @classmethod def public_deployments(cls): return Domain.view('domain/with_deployment', include_docs=True).all() @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 """ return get_db().view( "domain/snapshots", startkey=[self.get_id], endkey=[self.get_id, {}], reduce=True, include_docs=False, ).one()["value"] @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 ""
class Domain(Document, HQBillingDomainMixin, 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() is_public = BooleanProperty(default=False) date_created = DateTimeProperty() default_timezone = StringProperty(default=getattr(settings, "TIME_ZONE", "UTC")) case_sharing = BooleanProperty(default=False) organization = StringProperty() hr_name = StringProperty() # the human-readable name for this project within an organization eula = SchemaProperty(LicenseAgreement) 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 = BooleanProperty(default=True) description = StringProperty() short_description = StringProperty() is_shared = BooleanProperty(default=False) commtrack_enabled = BooleanProperty(default=False) call_center_config = SchemaProperty(CallCenterProperties) case_display = SchemaProperty(CaseDisplaySettings) # CommConnect settings survey_management_enabled = BooleanProperty(default=False) sms_case_registration_enabled = BooleanProperty(default=False) # Whether or not a case can register via sms sms_case_registration_type = StringProperty() # Case type to apply to cases registered via sms sms_case_registration_owner_id = StringProperty() # Owner to apply to cases registered via sms sms_case_registration_user_id = StringProperty() # Submitting user to apply to cases registered via sms sms_mobile_worker_registration_enabled = BooleanProperty(default=False) # Whether or not a mobile worker can register via sms default_sms_backend_id = 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) author = StringProperty() phone_model = StringProperty() attribution_notes = StringProperty() publisher = StringProperty(choices=["organization", "user"], default="user") yt_id = StringProperty() deployment = SchemaProperty(Deployment) image_path = StringProperty() image_type = StringProperty() migrations = SchemaProperty(DomainMigrations) cached_properties = DictProperty() internal = SchemaProperty(InternalProperties) # 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', 'country', 'region', 'customer_type') @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 not 'creating_user' in data: # should_save = True # from corehq.apps.users.models import CouchUser # admins = CouchUser.view("users/admins_by_domain", key=data["name"], reduce=False, include_docs=True).all() # if len(admins) == 1: # data["creating_user"] = admins[0].username # else: # data["creating_user"] = None if 'slug' in data and data["slug"]: data["hr_name"] = data["slug"] del data["slug"] self = super(Domain, cls).wrap(data) if self.get_id: self.apply_migrations() if should_save: self.save() return self @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: 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, ).all() else: return [] @classmethod def field_by_prefix(cls, field, prefix='', is_approved=True): # unichr(0xfff8) is something close to the highest character available res = cls.view("domain/fields_by_prefix", group=True, startkey=[field, is_approved, prefix], endkey=[field, is_approved, "%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] @classmethod def get_by_field(cls, field, value, is_approved=True): return cls.view('domain/fields_by_prefix', key=[field, is_approved, value], reduce=False, include_docs=True).all() def apply_migrations(self): self.migrations.apply(self) @staticmethod def all_for_user(user): if not hasattr(user,'get_profile'): # this had better be an anonymous user return [] from corehq.apps.users.models import CouchUser couch_user = CouchUser.from_django_user(user) if couch_user: domain_names = couch_user.get_domains() return Domain.view("domain/domains", keys=domain_names, reduce=False, include_docs=True).all() 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): from corehq.apps.app_manager.models import ApplicationBase return ApplicationBase.view('app_manager/applications_brief', startkey=[self.name], endkey=[self.name, {}]).all() 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 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 def all_users(self): from corehq.apps.users.models import CouchUser return CouchUser.by_domain(self.name) def has_shared_media(self): return False def recent_submissions(self): from corehq.apps.reports.util import make_form_couch_key key = make_form_couch_key(self.name) res = get_db().view('reports_forms/all_forms', startkey=key+[{}], endkey=key, descending=True, reduce=False, include_docs=False, limit=1).all() if len(res) > 0: # if there have been any submissions in the past 30 days return (datetime.now() <= datetime.strptime(res[0]['value']['submission_time'], "%Y-%m-%dT%H:%M:%SZ") + timedelta(days=30)) else: return False @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 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 try: raise ValueError('%r is not a valid domain name' % name) except ValueError: if settings.DEBUG: raise else: notify_exception(None, '%r is not a valid domain name' % name) return None extra_args = {'stale': settings.COUCH_STALE_QUERY} if not strict else {} result = cls.view("domain/domains", key=name, reduce=False, include_docs=True, **extra_args ).first() if result is None and not strict: # on the off chance this is a brand new domain, try with strict return cls.get_by_name(name, strict=True) return result @classmethod def get_by_organization(cls, organization): result = cls.view("domain/by_organization", startkey=[organization], endkey=[organization, {}], reduce=False, include_docs=True) return result @classmethod def get_by_organization_and_hrname(cls, organization, hr_name): result = cls.view("domain/by_organization", key=[organization, hr_name], reduce=False, include_docs=True) return result @classmethod def get_or_create_with_name(cls, name, is_active=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()) new_domain.save(**get_safe_write_kwargs()) return new_domain def password_format(self): """ This was a performance hit, so for now we'll just return 'a' no matter what # If a single application is alphanumeric, return alphanumeric; otherwise, return numeric """ # for app in self.full_applications(): # if hasattr(app, 'profile'): # format = app.profile.get('properties', {}).get('password_format', 'n') # if format == 'a': # return 'a' # return 'n' return 'a' @classmethod def get_all(cls, include_docs=True): return Domain.view("domain/not_snapshots", include_docs=include_docs).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_copy(self, new_domain_name=None, user=None): from corehq.apps.app_manager.models import get_app if new_domain_name is not None and Domain.get_by_name(new_domain_name): return None db = get_db() new_id = db.copy_doc(self.get_id)['id'] if new_domain_name is None: new_domain_name = new_id new_domain = Domain.get(new_id) new_domain.name = new_domain_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 the cda 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 for field in self._dirty_fields: if hasattr(new_domain, field): delattr(new_domain, field) for res in db.view('domain/related_to_domain', key=[self.name, True]): if not self.is_snapshot and res['value']['doc_type'] in ('Application', 'RemoteApp'): app = get_app(self.name, res['value']['_id']).get_latest_saved() if app: self.copy_component(app.doc_type, app._id, new_domain_name, user=user) else: self.copy_component(res['value']['doc_type'], res['value']['_id'], new_domain_name, user=user) else: self.copy_component(res['value']['doc_type'], res['value']['_id'], new_domain_name, user=user) new_domain.save() 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) return new_domain 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 str_to_cls = { 'UserRole': UserRole, } db = get_db() if doc_type in ('Application', 'RemoteApp'): new_doc = import_app(id, new_domain_name) new_doc.copy_history.append(id) else: cls = str_to_cls[doc_type] 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 self.is_snapshot and doc_type == 'Application': new_doc.prepare_multimedia_for_exchange() new_doc.save() return new_doc def save_snapshot(self): if self.is_snapshot: return self else: copy = self.save_copy() if copy is None: return None copy.is_snapshot = True copy.organization = self.organization # i don't think we want this? copy.snapshot_time = datetime.now() del copy.deployment copy.save() return copy def from_snapshot(self): return not self.is_snapshot and self.original_doc is not None def snapshots(self): return Domain.view('domain/snapshots', startkey=[self._id, {}], endkey=[self._id], include_docs=True, descending=True) @memoized def published_snapshot(self): snapshots = self.snapshots().all() for snapshot in snapshots: if snapshot.published: return snapshot return None @classmethod def published_snapshots(cls, include_unapproved=False, page=None, per_page=10): skip = None limit = None if page: skip = (page - 1) * per_page limit = per_page if include_unapproved: return cls.view('domain/published_snapshots', startkey=[False, {}], include_docs=True, descending=True, limit=limit, skip=skip) else: return cls.view('domain/published_snapshots', endkey=[True], include_docs=True, descending=True, limit=limit, skip=skip) @classmethod def snapshot_search(cls, query, page=None, per_page=10): skip = None limit = None if page: skip = (page - 1) * per_page limit = per_page results = get_db().search('domain/snapshot_search', q=json.dumps(query), limit=limit, skip=skip, stale='ok') return map(cls.get, [r['id'] for r in results]), results.total_rows @memoized def get_organization(self): from corehq.apps.orgs.models import Organization return Organization.get_by_name(self.organization) @memoized def organization_title(self): if self.organization: return self.get_organization().title else: return '' 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() if self.hr_name and self.organization: return self.hr_name else: return self.name def long_display_name(self): if self.is_snapshot: return format_html( "Snapshot of {0} > {1}", self.get_organization().title, self.copied_from.display_name() ) if self.organization: return format_html( '{0} > {1}', self.get_organization().title, self.hr_name or self.name ) else: return self.name __str__ = long_display_name def get_license_display(self): return LICENSES.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): # delete all associated objects db = get_db() related_docs = db.view('domain/related_to_domain', startkey=[self.name], endkey=[self.name, {}], include_docs=True) for doc in related_docs: db.delete_doc(doc['doc']) super(Domain, self).delete() 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: 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 popular_sort(cls, domains): sorted_list = [] MIN_REVIEWS = 1.0 domains = [(domain, Review.get_average_rating_by_app(domain.copied_from._id), Review.get_num_ratings_by_app(domain.copied_from._id)) for domain in domains] domains = [(domain, avg or 0.0, num or 0) for domain, avg, num in domains] total_average_sum = sum(avg for domain, avg, num in domains) total_average_count = len(domains) if not total_average_count: return [] total_average = (total_average_sum / total_average_count) for domain, average_rating, num_ratings in domains: if num_ratings == 0: sorted_list.append((0.0, domain)) else: weighted_rating = ((num_ratings / (num_ratings + MIN_REVIEWS)) * average_rating + (MIN_REVIEWS / (num_ratings + MIN_REVIEWS)) * total_average) sorted_list.append((weighted_rating, domain)) sorted_list = [domain for weighted_rating, domain in sorted(sorted_list, key=lambda domain: domain[0], reverse=True)] return sorted_list @classmethod def hit_sort(cls, domains): domains = list(domains) domains = sorted(domains, key=lambda domain: domain.downloads, reverse=True) return domains @classmethod def public_deployments(cls): return Domain.view('domain/with_deployment', include_docs=True).all() @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 = get_domain_module_map().get(domain_name, domain_name) try: return __import__(module_name) if module_name else None except ImportError: return None @property 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 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)
class ReportNotification(CachedCouchDocumentMixin, Document): domain = StringProperty() owner_id = StringProperty() recipient_emails = StringListProperty() config_ids = StringListProperty() send_to_owner = BooleanProperty() attach_excel = BooleanProperty() # language is only used if some of the config_ids refer to UCRs or custom reports language = StringProperty() email_subject = StringProperty(default=DEFAULT_REPORT_NOTIF_SUBJECT) hour = IntegerProperty(default=8) minute = IntegerProperty(default=0) day = IntegerProperty(default=1) interval = StringProperty(choices=["hourly", "daily", "weekly", "monthly"]) uuid = StringProperty() start_date = DateProperty(default=None) addedToBulk = BooleanProperty(default=False) @property def is_editable(self): try: self.report_slug return False except AttributeError: return True @classmethod def get_report(cls, report_id): try: notification = ReportNotification.get(report_id) except ResourceNotFound: notification = None else: if notification.doc_type != 'ReportNotification': notification = None return notification @classmethod def by_domain(cls, domain, stale=True, **kwargs): if stale: kwargs['stale'] = settings.COUCH_STALE_QUERY key = [domain] return cls._get_view_by_key(key, **kwargs) @classmethod def by_domain_and_owner(cls, domain, owner_id, stale=True, **kwargs): if stale: kwargs['stale'] = settings.COUCH_STALE_QUERY key = [domain, owner_id] return cls._get_view_by_key(key, **kwargs) @classmethod def _get_view_by_key(cls, key, **kwargs): db = cls.get_db() result = cache_core.cached_view(db, "reportconfig/user_notifications", reduce=False, include_docs=True, startkey=key, endkey=key + [{}], wrapper=cls.wrap, **kwargs) return result @property @memoized def all_recipient_emails(self): emails = frozenset(self.recipient_emails) if self.send_to_owner and self.owner_email: emails |= {self.owner_email} return emails @property @memoized def owner_email(self): if self.owner.is_web_user(): return self.owner.username email = self.owner.get_email() try: validate_email(email) return email except Exception: pass @property @memoized def owner(self): id = self.owner_id return CouchUser.get_by_user_id(id) @property @memoized def configs(self): """ Access the notification's associated configs as a list, transparently returning an appropriate dummy for old notifications which have `report_slug` instead of `config_ids`. """ if self.config_ids: configs = [] for config_doc in iter_docs(ReportConfig.get_db(), self.config_ids): config = ReportConfig.wrap(config_doc) if not hasattr(config, 'deleted'): configs.append(config) def _sort_key(config_id): if config_id in self.config_ids: return self.config_ids.index(config_id) else: return len(self.config_ids) configs = sorted(configs, key=_sort_key) elif self.report_slug == 'admin_domains': raise UnsupportedScheduledReportError( "admin_domains is no longer " "supported as a schedulable report for the time being") else: # create a new ReportConfig object, useful for its methods and # calculated properties, but don't save it class ReadonlyReportConfig(ReportConfig): def save(self, *args, **kwargs): pass config = ReadonlyReportConfig() object.__setattr__(config, '_id', 'dummy') config.report_type = ProjectReportDispatcher.prefix config.report_slug = self.report_slug config.domain = self.domain config.owner_id = self.owner_id configs = [config] return tuple(configs) @property def day_name(self): if self.interval == 'hourly': return _("Every hour") if self.interval == 'weekly': return calendar.day_name[self.day] return { "daily": _("Every day"), "monthly": _("Day %s of every month" % self.day), }[self.interval] @classmethod def day_choices(cls): """Tuples for day of week number and human-readable day of week""" return tuple([(val, calendar.day_name[val]) for val in range(7)]) @classmethod def hour_choices(cls): """Tuples for hour number and human-readable hour""" return tuple([(val, "%s:00" % val) for val in range(24)]) @property @memoized def recipients_by_language(self): user_languages = { user['username']: user['language'] for user in get_user_docs_by_username(self.all_recipient_emails) if 'username' in user and 'language' in user } if self.language: fallback_language = self.language else: fallback_language = user_languages.get(self.owner_email, 'en') recipients = defaultdict(list) for email in self.all_recipient_emails: language = user_languages.get( email, fallback_language) or fallback_language recipients[language].append(email) return immutabledict(recipients) def get_secret(self, email): uuid = self._get_or_create_uuid() return hashlib.sha1((uuid + email).encode('utf-8')).hexdigest()[:20] def send(self): # Scenario: user has been removed from the domain that they # have scheduled reports for. Delete this scheduled report if not self.owner.is_member_of(self.domain, allow_enterprise=True): self.delete() return if self.owner.is_deleted(): self.delete() return if self.recipients_by_language: for language, emails in self.recipients_by_language.items(): self._get_and_send_report(language, emails) def _get_or_create_uuid(self): if not self.uuid: self.uuid = uuid.uuid4().hex self.save() return self.uuid def _get_and_send_report(self, language, emails): with localize(language): title = self._get_title(self.email_subject) attach_excel = getattr(self, 'attach_excel', False) report_text, excel_files = self._generate_report( attach_excel, title, emails) # Both are empty if ALL the ReportConfigs in the ReportNotification # have a start_date in the future (or an exception occurred) if not report_text and not excel_files: return self._send_emails(title, report_text, emails, excel_files) @staticmethod def _get_title(subject): # only translate the default subject return (_(DEFAULT_REPORT_NOTIF_SUBJECT) if subject == DEFAULT_REPORT_NOTIF_SUBJECT else subject) def _generate_report(self, attach_excel, title, emails): from corehq.apps.reports.views import get_scheduled_report_response report_text = None excel_files = None try: report_text, excel_files = get_scheduled_report_response( self.owner, self.domain, self._id, attach_excel=attach_excel, send_only_active=True) # TODO: Be more specific with our error-handling. If building the report could fail, # we should have a reasonable idea of why except Exception as er: notify_exception( None, message="Encountered error while generating report", details={ 'subject': title, 'recipients': str(emails), 'error': er, }) if isinstance(er, ESError): # An ElasticSearch error could indicate that the report itself is too large. # Try exporting the report instead, as that builds the report in chunks, # rather than all at once. # TODO: narrow down this handling so that we don't try this if ElasticSearch is simply down, # for example self._export_report(emails, title) return report_text, excel_files def _send_emails(self, title, report_text, emails, excel_files): from corehq.apps.reports.views import render_full_report_notification email_is_too_large = False for email in emails: body = render_full_report_notification(None, report_text, email, self).content try: self._send_email(title, email, body, excel_files) except Exception as er: if isinstance(er, SMTPSenderRefused) and ( er.smtp_code in LARGE_FILE_SIZE_ERROR_CODES): email_is_too_large = True break else: ScheduledReportLogger.log_email_failure( self, email, body, er) else: ScheduledReportLogger.log_email_success(self, email, body) if email_is_too_large: # TODO: Because different domains may have different size thresholds, # one of the middle addresses could have triggered this, causing us to send # both the original email and the retried email to some users. # This is likely best handled by treating each address separately. ScheduledReportLogger.log_email_size_failure( self, email, emails, body) # The body is too large -- attempt to resend the report as attachments. if excel_files: # If the attachments already exist, just send them self._send_only_attachments(title, emails, excel_files) else: # Otherwise we're forced to trigger a process to create them self._export_report(emails, title) def _send_email(self, title, email, body, excel_files): send_HTML_email(title, email, body, email_from=settings.DEFAULT_FROM_EMAIL, file_attachments=excel_files, smtp_exception_skip_list=LARGE_FILE_SIZE_ERROR_CODES) def _send_only_attachments(self, title, emails, excel_files): message = _( "Unable to generate email report. Excel files are attached.") send_HTML_email(title, emails, message, email_from=settings.DEFAULT_FROM_EMAIL, file_attachments=excel_files) def _export_report(self, emails, title): from corehq.apps.reports.standard.deployments import ApplicationStatusReport for report_config in self.configs: mock_request = HttpRequest() mock_request.couch_user = self.owner mock_request.user = self.owner.get_django_user() mock_request.domain = self.domain mock_request.couch_user.current_domain = self.domain mock_request.couch_user.language = self.language mock_request.method = 'GET' mock_request.bypass_two_factor = True mock_query_string_parts = [ report_config.query_string, 'filterSet=true' ] mock_request.GET = QueryDict('&'.join(mock_query_string_parts)) request_data = vars(mock_request) request_data['couch_user'] = mock_request.couch_user.userID if report_config.report_slug != ApplicationStatusReport.slug: # ApplicationStatusReport doesn't have date filter date_range = report_config.get_date_range() start_date = datetime.strptime(date_range['startdate'], '%Y-%m-%d') end_date = datetime.strptime(date_range['enddate'], '%Y-%m-%d') datespan = DateSpan(start_date, end_date) request_data['datespan'] = datespan full_request = { 'request': request_data, 'domain': request_data['domain'], 'context': {}, 'request_params': json_request(request_data['GET']) } export_all_rows_task(report_config.report, full_request, emails, title) def remove_recipient(self, email): try: if email == self.owner.get_email(): self.send_to_owner = False self.recipient_emails.remove(email) except ValueError: pass def update_attributes(self, items): for k, v in items: if k == 'start_date': self.verify_start_date(v) self.__setattr__(k, v) def verify_start_date(self, start_date): if start_date != self.start_date and start_date < datetime.today( ).date(): raise ValidationError( "You can not specify a start date in the past.") def can_be_viewed_by(self, user): return ((user._id == self.owner_id) or (user.is_domain_admin(self.domain) or (user.get_email() in self.all_recipient_emails)))
class SavedExportSchema(BaseSavedExportSchema, UnicodeMixIn): """ Lets you save an export format with a schema and list of columns and display names. """ name = StringProperty() default_format = StringProperty() is_safe = BooleanProperty(default=False) # self.index should always match self.schema.index # needs to be here so we can use in couch views index = JsonProperty() # id of an ExportSchema for checkpointed schemas schema_id = StringProperty() # user-defined table configuration tables = SchemaListProperty(ExportTable) # For us right now, 'form' or 'case' type = StringProperty() def __unicode__(self): return "%s (%s)" % (self.name, self.index) def transform(self, doc): return doc @property def global_transform_function(self): # will be called on every value in the doc during export return identity @property @memoized def schema(self): return ExportSchema.get(self.schema_id) @property def table_name(self): return self.sheet_name if self.sheet_name else "%s" % self._id @classmethod def default(cls, schema, name="", type='form'): return cls(name=name, index=schema.index, schema_id=schema.get_id, tables=[ExportTable.default(schema.tables[0][0])], type=type) @property @memoized def tables_by_index(self): return dict([t.index, t] for t in self.tables) def get_table_configuration(self, index): def column_configuration(): columns = self.schema.get_columns(index) if self.tables_by_index.has_key(index): return list( self.tables_by_index[index].get_column_configuration( columns)) else: return [ ExportColumn(index=c, display='').to_config_format(selected=False) for c in columns ] def display(): if self.tables_by_index.has_key(index): return self.tables_by_index[index].display else: return '' return { "index": index, "display": display(), "column_configuration": column_configuration(), "selected": index in self.tables_by_index } def get_table_headers(self, override_name=False): return ((self.table_name if override_name and i == 0 else t.index, [t.get_headers_row()]) for i, t in enumerate(self.tables)) @property def table_configuration(self): return [ self.get_table_configuration(index) for index, cols in self.schema.tables ] def update_schema(self): """ Update the schema for this object to include the latest columns from any relevant docs. Does NOT save the doc, just updates the in-memory object. """ from couchexport.schema import build_latest_schema self.set_schema(build_latest_schema(self.index)) def set_schema(self, schema): """ Set the schema for this object. Does NOT save the doc, just updates the in-memory object. """ self.schema_id = schema.get_id def trim(self, document_table, doc, apply_transforms=True): for table_index, data in document_table: if self.tables_by_index.has_key(table_index): # todo: currently (index, rows) instead of (display, rows); where best to convert to display? yield (table_index, self.tables_by_index[table_index].trim( data, doc, apply_transforms, self.global_transform_function)) def get_export_components(self, previous_export_id=None, filter=None): from couchexport.export import ExportConfiguration database = get_db() config = ExportConfiguration(database, self.index, previous_export_id, self.filter & filter) # get and checkpoint the latest schema updated_schema = config.get_latest_schema() export_schema_checkpoint = config.create_new_checkpoint() return config, updated_schema, export_schema_checkpoint def get_export_files(self, format=None, previous_export=None, filter=None, process=None, max_column_size=None, apply_transforms=True, limit=0, **kwargs): from couchexport.export import get_writer, format_tables, create_intermediate_tables if not format: format = self.default_format or Format.XLS_2007 config, updated_schema, export_schema_checkpoint = self.get_export_components( previous_export, filter) # transform docs onto output and save writer = get_writer(format) # open the doc and the headers formatted_headers = list(self.get_table_headers()) fd, path = tempfile.mkstemp() with os.fdopen(fd, 'wb') as tmp: writer.open(formatted_headers, tmp, max_column_size=max_column_size, table_titles=dict([(table.index, table.display) for table in self.tables if table.display])) total_docs = len(config.potentially_relevant_ids) if process: DownloadBase.set_progress(process, 0, total_docs) for i, doc in config.enum_docs(): if limit and i > limit: break if self.transform and apply_transforms: doc = self.transform(doc) formatted_tables = self.trim(format_tables( create_intermediate_tables(doc, updated_schema), separator="."), doc, apply_transforms=apply_transforms) writer.write(formatted_tables) if process: DownloadBase.set_progress(process, i + 1, total_docs) writer.close() return ExportFiles(path, export_schema_checkpoint, format) def download_data(self, format="", previous_export=None, filter=None, limit=0): """ If there is data, return an HTTPResponse with the appropriate data. If there is not data returns None. """ from couchexport.shortcuts import export_response files = self.get_export_files(format, previous_export, filter, limit=limit) return export_response(files.file, files.format, self.name) def to_export_config(self): """ Return an ExportConfiguration object that represents this. """ # confusingly, the index isn't the actual index property, # but is the index appended with the id to this document. # this is to avoid conflicts among multiple exports index = "%s-%s" % (self.index, self._id) if isinstance(self.index, basestring) else \ self.index + [self._id] # self.index required to be a string or list return ExportConfiguration(index=index, name=self.name, format=self.default_format) def custom_validate(self): if self.default_format == Format.XLS: for table in self.tables: if len(table.columns) > 255: raise CustomExportValidationError( "XLS files can only have 255 columns") # replaces `sheet_name = StringProperty()` def __get_sheet_name(self): return self.tables[0].display def __set_sheet_name(self, value): self.tables[0].display = value sheet_name = property(__get_sheet_name, __set_sheet_name) @classmethod def wrap(cls, data): # since this is a property now, trying to wrap it will fail hard if 'sheet_name' in data: del data['sheet_name'] return super(SavedExportSchema, cls).wrap(data)
class ComputedDocumentMixin(DocumentSchema): """ Use this mixin for things like CommCareCase or XFormInstance documents that take advantage of indicator definitions. computed_ is namespaced and may look like the following for indicators: computed_: { mvp_indicators: { indicator_slug: { version: 1, value: "foo" } } } """ computed_ = DictProperty() computed_modified_on_ = DateTimeProperty() # a flag for the indicator pillows so that there aren't any Document Update Conflicts initial_processing_complete = BooleanProperty(default=False) def update_indicator(self, indicator_def, save_on_update=True, logger=None): existing_indicators = self.computed_.get(indicator_def.namespace, {}) updated_indicators, is_update = indicator_def.update_computed_namespace( existing_indicators, self) if is_update: self.computed_[indicator_def.namespace] = updated_indicators self.computed_modified_on_ = datetime.datetime.utcnow() if logger: logger.info( "[INDICATOR %(namespace)s %(domain)s] Updating %(indicator_type)s:%(indicator_slug)s " "in %(document_type)s [%(document_id)s]." % { 'namespace': indicator_def.namespace, 'domain': indicator_def.domain, 'indicator_type': indicator_def.__class__.__name__, 'indicator_slug': indicator_def.slug, 'document_type': self.__class__.__name__, 'document_id': self._id, }) if save_on_update: self.save(**get_safe_write_kwargs()) if logger: logger.debug("Saved %s." % self._id) return is_update def update_indicators_in_bulk(self, indicators, save_on_update=True, logger=None): is_update = False for indicator in indicators: try: if self.update_indicator(indicator, save_on_update=False, logger=logger): is_update = True except Exception: logger.exception( "[INDICATOR %(namespace)s %(domain)s] Failed to update %(indicator_type)s: " "%(indicator_slug)s in %(document_type)s [%(document_id)s]." % { 'namespace': indicator.namespace, 'domain': indicator.domain, 'indicator_type': indicator.__class__.__name__, 'indicator_slug': indicator.slug, 'document_type': self.__class__.__name__, 'document_id': self._id, }) if is_update and save_on_update: try: self.save(**get_safe_write_kwargs()) if logger: logger.info("Saved %s." % self._id) except ResourceConflict: logger.error( "[INDICATOR %(domain)s] Resource conflict failed to save document indicators for " "%(document_type)s [%(document_id)s]." % { 'domain': self.domain, 'document_type': self.__class__.__name__, 'document_id': self._id, }) return is_update
class CallCenterProperties(DocumentSchema): enabled = BooleanProperty(default=False) case_owner_id = StringProperty() case_type = StringProperty()
class DynamicReportConfig(DocumentSchema): """configurations of generic/template reports to be set up for this domain""" report = StringProperty() # fully-qualified path to template report class name = StringProperty() # report display name in sidebar kwargs = DictProperty() # arbitrary settings to configure report previewers_only = BooleanProperty()
class ReportNotification(CachedCouchDocumentMixin, Document): domain = StringProperty() owner_id = StringProperty() recipient_emails = StringListProperty() config_ids = StringListProperty() send_to_owner = BooleanProperty() attach_excel = BooleanProperty() # language is only used if some of the config_ids refer to UCRs or custom reports language = StringProperty() email_subject = StringProperty(default=DEFAULT_REPORT_NOTIF_SUBJECT) hour = IntegerProperty(default=8) minute = IntegerProperty(default=0) day = IntegerProperty(default=1) interval = StringProperty(choices=["daily", "weekly", "monthly"]) uuid = StringProperty() start_date = DateProperty(default=None) @property def is_editable(self): try: self.report_slug return False except AttributeError: return True @classmethod def by_domain_and_owner(cls, domain, owner_id, stale=True, **kwargs): if stale: kwargs['stale'] = settings.COUCH_STALE_QUERY key = [domain, owner_id] db = cls.get_db() result = cache_core.cached_view(db, "reportconfig/user_notifications", reduce=False, include_docs=True, startkey=key, endkey=key + [{}], wrapper=cls.wrap, **kwargs) return result @property @memoized def all_recipient_emails(self): # handle old documents if not self.owner_id: return frozenset([self.owner.get_email()]) emails = frozenset(self.recipient_emails) if self.send_to_owner and self.owner_email: emails |= {self.owner_email} return emails @property @memoized def owner_email(self): if self.owner.is_web_user(): return self.owner.username email = self.owner.get_email() try: validate_email(email) return email except Exception: pass @property @memoized def owner(self): id = self.owner_id return CouchUser.get_by_user_id(id) @property @memoized def configs(self): """ Access the notification's associated configs as a list, transparently returning an appropriate dummy for old notifications which have `report_slug` instead of `config_ids`. """ if self.config_ids: configs = [] for config_doc in iter_docs(ReportConfig.get_db(), self.config_ids): config = ReportConfig.wrap(config_doc) if not hasattr(config, 'deleted'): configs.append(config) def _sort_key(config_id): if config_id in self.config_ids: return self.config_ids.index(config_id) else: return len(self.config_ids) configs = sorted(configs, key=_sort_key) elif self.report_slug == 'admin_domains': raise UnsupportedScheduledReportError( "admin_domains is no longer " "supported as a schedulable report for the time being") else: # create a new ReportConfig object, useful for its methods and # calculated properties, but don't save it class ReadonlyReportConfig(ReportConfig): def save(self, *args, **kwargs): pass config = ReadonlyReportConfig() object.__setattr__(config, '_id', 'dummy') config.report_type = ProjectReportDispatcher.prefix config.report_slug = self.report_slug config.domain = self.domain config.owner_id = self.owner_id configs = [config] return tuple(configs) @property def day_name(self): if self.interval == 'weekly': return calendar.day_name[self.day] return { "daily": _("Every day"), "monthly": _("Day %s of every month" % self.day), }[self.interval] @classmethod def day_choices(cls): """Tuples for day of week number and human-readable day of week""" return tuple([(val, calendar.day_name[val]) for val in range(7)]) @classmethod def hour_choices(cls): """Tuples for hour number and human-readable hour""" return tuple([(val, "%s:00" % val) for val in range(24)]) @property @memoized def recipients_by_language(self): user_languages = { user['username']: user['language'] for user in get_user_docs_by_username(self.all_recipient_emails) if 'username' in user and 'language' in user } if self.language: fallback_language = self.language else: fallback_language = user_languages.get(self.owner_email, 'en') recipients = defaultdict(list) for email in self.all_recipient_emails: language = user_languages.get(email, fallback_language) recipients[language].append(email) return immutabledict(recipients) def get_secret(self, email): uuid = self._get_or_create_uuid() return hashlib.sha1((uuid + email).encode('utf-8')).hexdigest()[:20] def send(self): # Scenario: user has been removed from the domain that they # have scheduled reports for. Delete this scheduled report if not self.owner.is_member_of(self.domain): self.delete() return if self.owner.is_deleted(): self.delete() return if self.recipients_by_language: for language, emails in self.recipients_by_language.items(): self._get_and_send_report(language, emails) def _get_or_create_uuid(self): if not self.uuid: self.uuid = uuid.uuid4().hex self.save() return self.uuid def _get_and_send_report(self, language, emails): from corehq.apps.reports.views import get_scheduled_report_response, render_full_report_notification with localize(language): title = (_(DEFAULT_REPORT_NOTIF_SUBJECT) if self.email_subject == DEFAULT_REPORT_NOTIF_SUBJECT else self.email_subject) attach_excel = getattr(self, 'attach_excel', False) try: content, excel_files = get_scheduled_report_response( self.owner, self.domain, self._id, attach_excel=attach_excel, send_only_active=True) # Will be False if ALL the ReportConfigs in the ReportNotification # have a start_date in the future. if content is False: return for email in emails: body = render_full_report_notification( None, content, email, self).content send_html_email_async( title, email, body, email_from=settings.DEFAULT_FROM_EMAIL, file_attachments=excel_files, smtp_exception_skip_list=LARGE_FILE_SIZE_ERROR_CODES) except Exception as er: notify_exception( None, message= "Encountered error while generating report or sending email", details={ 'subject': title, 'recipients': str(emails), 'error': er, }) if getattr(er, 'smtp_code', None) in LARGE_FILE_SIZE_ERROR_CODES or type( er) == ESError: # If the email doesn't work because it is too large to fit in the HTML body, # send it as an excel attachment, by creating a mock request with the right data. for report_config in self.configs: mock_request = HttpRequest() mock_request.couch_user = self.owner mock_request.user = self.owner.get_django_user() mock_request.domain = self.domain mock_request.couch_user.current_domain = self.domain mock_request.couch_user.language = self.language mock_request.method = 'GET' mock_request.bypass_two_factor = True mock_query_string_parts = [ report_config.query_string, 'filterSet=true' ] if report_config.is_configurable_report: mock_query_string_parts.append( urlencode(report_config.filters, True)) mock_query_string_parts.append( urlencode(report_config.get_date_range(), True)) mock_request.GET = QueryDict( '&'.join(mock_query_string_parts)) date_range = report_config.get_date_range() start_date = datetime.strptime(date_range['startdate'], '%Y-%m-%d') end_date = datetime.strptime(date_range['enddate'], '%Y-%m-%d') datespan = DateSpan(start_date, end_date) request_data = vars(mock_request) request_data[ 'couch_user'] = mock_request.couch_user.userID request_data['datespan'] = datespan full_request = { 'request': request_data, 'domain': request_data['domain'], 'context': {}, 'request_params': json_request(request_data['GET']) } export_all_rows_task(report_config.report, full_request, emails, title) def remove_recipient(self, email): try: if email == self.owner.get_email(): self.send_to_owner = False self.recipient_emails.remove(email) except ValueError: pass def update_attributes(self, items): for k, v in items: if k == 'start_date': self.verify_start_date(v) self.__setattr__(k, v) def verify_start_date(self, start_date): if start_date != self.start_date and start_date < datetime.today( ).date(): raise ValidationError( "You can not specify a start date in the past.")
class XFormsSession(Document): """ Keeps information about an SMS XForm session. """ # generic properties connection_id = StringProperty() session_id = StringProperty() form_xmlns = StringProperty() start_time = DateTimeProperty() modified_time = DateTimeProperty() end_time = DateTimeProperty() completed = BooleanProperty() # HQ specific properties domain = StringProperty() user_id = StringProperty() app_id = StringProperty() submission_id = StringProperty() survey_incentive = StringProperty() session_type = StringProperty(choices=XFORMS_SESSION_TYPES, default=XFORMS_SESSION_SMS) workflow = StringProperty( ) # One of the corehq.apps.sms.models.WORKFLOW_* constants describing what kind of workflow this session was a part of reminder_id = StringProperty( ) # Points to the _id of an instance of corehq.apps.reminders.models.CaseReminder that this session is tied to def save(self, *args, **kwargs): if is_bigcouch() and "w" not in kwargs: # Force a write to all nodes before returning kwargs["w"] = bigcouch_quorum_count() return super(XFormsSession, self).save(*args, **kwargs) def __unicode__(self): return 'Form %(form)s in domain %(domain)s. Last modified: %(mod)s' % \ {"form": self.form_xmlns, "domain": self.domain, "mod": self.modified_time} def end(self, completed): """ Marks this as ended (by setting end time). """ self.completed = completed self.modified_time = self.end_time = datetime.utcnow() @property def is_open(self): """ True if this session is still open, False otherwise. """ return self.end_time is None @classmethod def get_all_open_sms_sessions(cls, domain, contact_id): sessions = cls.view("smsforms/open_sms_sessions_by_connection", key=[domain, contact_id], include_docs=True).all() return sessions @classmethod def close_all_open_sms_sessions(cls, domain, contact_id): sessions = cls.get_all_open_sms_sessions(domain, contact_id) for session in sessions: session.end(False) session.save() @classmethod def latest_by_session_id(cls, id): return XFormsSession.view("smsforms/sessions_by_touchforms_id", startkey=[id], endkey=[id, {}], include_docs=True).one() @classmethod def get_open_sms_session(cls, domain, contact_id): """ Looks up the open sms survey session for the given domain and contact_id. Only one session is expected to be open at a time. Raises MultipleResultsFound if more than one session is open. """ session = cls.view("smsforms/open_sms_sessions_by_connection", key=[domain, contact_id], include_docs=True).one() return session