예제 #1
0
class FixtureReportResult(Document, QueryMixin):
    domain = StringProperty()
    location_id = StringProperty()
    start_date = DateProperty()
    end_date = DateProperty()
    report_slug = StringProperty()
    rows = DictProperty()
    name = StringProperty()

    class Meta:
        app_label = "m4change"

    @classmethod
    def by_composite_key(cls, domain, location_id, start_date, end_date,
                         report_slug):
        try:
            return cls.view(
                "m4change/fixture_by_composite_key",
                key=[domain, location_id, start_date, end_date, report_slug],
                include_docs=True).one(except_all=True)
        except (NoResultFound, ResourceNotFound, MultipleResultsFound):
            return None

    @classmethod
    def by_domain(cls, domain):
        return cls.view("m4change/fixture_by_composite_key",
                        startkey=[domain],
                        endkey=[domain, {}],
                        include_docs=True).all()

    @classmethod
    def get_report_results_by_key(cls, domain, location_id, start_date,
                                  end_date):
        return cls.view("m4change/fixture_by_composite_key",
                        startkey=[domain, location_id, start_date, end_date],
                        endkey=[domain, location_id, start_date, end_date, {}],
                        include_docs=True).all()

    @classmethod
    def _validate_params(cls, params):
        for param in params:
            if param is None or len(param) == 0:
                return False
        return True

    @classmethod
    def save_result(cls, domain, location_id, start_date, end_date,
                    report_slug, rows, name):
        if not cls._validate_params([domain, location_id, report_slug]) \
                or not isinstance(rows, dict) or len(rows) == 0 \
                or not isinstance(start_date, date) or not isinstance(end_date, date):
            return
        FixtureReportResult(domain=domain,
                            location_id=location_id,
                            start_date=start_date,
                            end_date=end_date,
                            report_slug=report_slug,
                            rows=rows,
                            name=name).save()
예제 #2
0
class CObservationAddendum(Document):
    observed_date = DateProperty()
    art_observations = SchemaListProperty(CObservation)
    nonart_observations = SchemaListProperty(CObservation)
    created_by = StringProperty()
    created_date = DateTimeProperty()
    notes = StringProperty()  # placeholder if need be
    class Meta:
        app_label = 'pact'
예제 #3
0
class ReportConfig(CachedCouchDocumentMixin, Document):
    """
    This model represents a "Saved Report." That is, a saved set of filters for a given report.
    """

    domain = StringProperty()

    # the prefix of the report dispatcher class for this report, used to
    # get route name for url reversing, and report names
    report_type = StringProperty()
    report_slug = StringProperty()
    subreport_slug = StringProperty(default=None)

    name = StringProperty()
    description = StringProperty()
    owner_id = StringProperty()

    filters = DictProperty()

    date_range = StringProperty(choices=get_all_daterange_slugs())
    days = IntegerProperty(default=None)
    start_date = DateProperty(default=None)
    end_date = DateProperty(default=None)
    datespan_slug = StringProperty(default=None)

    def delete(self, *args, **kwargs):
        notifications = self.view('reportconfig/notifications_by_config',
                                  reduce=False,
                                  include_docs=True,
                                  key=self._id).all()

        for n in notifications:
            n.config_ids.remove(self._id)
            if n.config_ids:
                n.save()
            else:
                n.delete()

        return super(ReportConfig, self).delete(*args, **kwargs)

    @classmethod
    def by_domain_and_owner(cls,
                            domain,
                            owner_id,
                            report_slug=None,
                            stale=True,
                            skip=None,
                            limit=None):
        kwargs = {}
        if stale:
            kwargs['stale'] = settings.COUCH_STALE_QUERY

        if report_slug is not None:
            key = ["name slug", domain, owner_id, report_slug]
        else:
            key = ["name", domain, owner_id]

        db = cls.get_db()
        if skip is not None:
            kwargs['skip'] = skip
        if limit is not None:
            kwargs['limit'] = limit

        result = cache_core.cached_view(db,
                                        "reportconfig/configs_by_domain",
                                        reduce=False,
                                        include_docs=True,
                                        startkey=key,
                                        endkey=key + [{}],
                                        wrapper=cls.wrap,
                                        **kwargs)
        return result

    @classmethod
    def default(self):
        return {
            'name': '',
            'description': '',
            #'date_range': 'last7',
            'days': None,
            'start_date': None,
            'end_date': None,
            'filters': {}
        }

    def to_complete_json(self, lang=None):
        result = super(ReportConfig, self).to_json()
        result.update({
            'url':
            self.url,
            'report_name':
            self.report_name,
            'date_description':
            self.date_description,
            'datespan_filters':
            self.datespan_filter_choices(self.datespan_filters, lang
                                         or ucr_default_language()),
            'has_ucr_datespan':
            self.has_ucr_datespan,
        })
        return result

    @property
    @memoized
    def _dispatcher(self):
        from corehq.apps.userreports.models import CUSTOM_REPORT_PREFIX
        from corehq.apps.userreports.reports.view import (
            ConfigurableReportView,
            CustomConfigurableReportDispatcher,
        )

        dispatchers = [
            ProjectReportDispatcher,
            CustomProjectReportDispatcher,
        ]

        for dispatcher in dispatchers:
            if dispatcher.prefix == self.report_type:
                return dispatcher()

        if self.report_type == 'configurable':
            if self.subreport_slug.startswith(CUSTOM_REPORT_PREFIX):
                return CustomConfigurableReportDispatcher()
            else:
                return ConfigurableReportView()

        if self.doc_type != 'ReportConfig-Deleted':
            self.doc_type += '-Deleted'
            self.save()
            notify_exception(
                None,
                "This saved-report (id: %s) is unknown (report_type: %s) and so we have archived it"
                % (self._id, self.report_type))
        raise UnsupportedSavedReportError("Unknown dispatcher: %s" %
                                          self.report_type)

    def get_date_range(self):
        date_range = self.date_range
        # allow old report email notifications to represent themselves as a
        # report config by leaving the default date range up to the report
        # dispatcher
        if not date_range:
            return {}

        try:
            start_date, end_date = get_daterange_start_end_dates(
                date_range,
                start_date=self.start_date,
                end_date=self.end_date,
                days=self.days,
            )
        except InvalidDaterangeException:
            # this is due to bad validation. see: http://manage.dimagi.com/default.asp?110906
            logging.error(
                'saved report %s is in a bad state - date range is misconfigured'
                % self._id)
            return {}

        dates = {
            'startdate': start_date.isoformat(),
            'enddate': end_date.isoformat(),
        }

        if self.is_configurable_report:
            filter_slug = self.datespan_slug
            if filter_slug:
                return {
                    '%s-start' % filter_slug: start_date.isoformat(),
                    '%s-end' % filter_slug: end_date.isoformat(),
                    filter_slug: '%(startdate)s to %(enddate)s' % dates,
                }
        return dates

    @property
    @memoized
    def query_string(self):
        params = {}
        if self._id != 'dummy':
            params['config_id'] = self._id
        params.update(self.filters)
        params.update(self.get_date_range())

        return urlencode(params, True)

    @property
    @memoized
    def url_kwargs(self):
        kwargs = {
            'domain': self.domain,
            'report_slug': self.report_slug,
        }

        if self.subreport_slug:
            kwargs['subreport_slug'] = self.subreport_slug

        return immutabledict(kwargs)

    @property
    @memoized
    def view_kwargs(self):
        if not self.is_configurable_report:
            return self.url_kwargs.union({
                'permissions_check':
                self._dispatcher.permissions_check,
            })
        return self.url_kwargs

    @property
    @memoized
    def url(self):
        try:
            if self.is_configurable_report:
                url_base = absolute_reverse(
                    self.report_slug, args=[self.domain, self.subreport_slug])
            else:
                url_base = absolute_reverse(self._dispatcher.name(),
                                            kwargs=self.url_kwargs)
            return url_base + '?' + self.query_string
        except UnsupportedSavedReportError:
            return "#"
        except Exception as e:
            logging.exception(str(e))
            return "#"

    @property
    @memoized
    def report(self):
        """
        Returns None if no report is found for that report slug, which happens
        when a report is no longer available.  All callers should handle this
        case.

        """
        try:
            return self._dispatcher.get_report(self.domain, self.report_slug,
                                               self.subreport_slug)
        except UnsupportedSavedReportError:
            return None

    @property
    def report_name(self):
        try:
            if self.report is None:
                return _("Deleted Report")
            else:
                return _(self.report.name)
        except Exception:
            return _("Unsupported Report")

    @property
    def full_name(self):
        if self.name:
            return "%s (%s)" % (self.name, self.report_name)
        else:
            return self.report_name

    @property
    def date_description(self):
        if self.date_range == 'lastmonth':
            return "Last Month"
        elif self.days and not self.start_date:
            day = 'day' if self.days == 1 else 'days'
            return "Last %d %s" % (self.days, day)
        elif self.end_date:
            return "From %s to %s" % (self.start_date, self.end_date)
        elif self.start_date:
            return "Since %s" % self.start_date
        else:
            return ''

    @property
    @memoized
    def owner(self):
        return CouchUser.get_by_user_id(self.owner_id)

    def get_report_content(self, lang, attach_excel=False):
        """
        Get the report's HTML content as rendered by the static view format.

        """
        from corehq.apps.locations.middleware import LocationAccessMiddleware

        try:
            if self.report is None:
                return ReportContent(
                    _("The report used to create this scheduled report is no"
                      " longer available on CommCare HQ.  Please delete this"
                      " scheduled report and create a new one using an available"
                      " report."),
                    None,
                )
        except Exception:
            pass

        if getattr(self.report, 'is_deprecated', False):
            return ReportContent(
                self.report.deprecation_email_message or
                _("[DEPRECATED] %s report has been deprecated and will stop working soon. "
                  "Please update your saved reports email settings if needed."
                  % self.report.name),
                None,
            )

        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 = lang
        mock_request.method = 'GET'
        mock_request.bypass_two_factor = True

        mock_query_string_parts = [self.query_string, 'filterSet=true']
        if self.is_configurable_report:
            mock_query_string_parts.append(urlencode(self.filters, True))
            mock_query_string_parts.append(
                urlencode(self.get_date_range(), True))
        mock_request.GET = QueryDict('&'.join(mock_query_string_parts))

        # Make sure the request gets processed by PRBAC Middleware
        CCHQPRBACMiddleware.apply_prbac(mock_request)
        LocationAccessMiddleware.apply_location_access(mock_request)

        try:
            dispatch_func = functools.partial(
                self._dispatcher.__class__.as_view(), mock_request,
                **self.view_kwargs)
            email_response = dispatch_func(render_as='email')
            if email_response.status_code == 302:
                return ReportContent(
                    _("We are sorry, but your saved report '%(config_name)s' "
                      "is no longer accessible because the owner %(username)s "
                      "is no longer active.") % {
                          'config_name': self.name,
                          'username': self.owner.username
                      },
                    None,
                )
            try:
                content_json = json.loads(email_response.content)
            except ValueError:
                email_text = email_response.content
            else:
                email_text = content_json['report']
            excel_attachment = dispatch_func(
                render_as='excel') if attach_excel else None
            return ReportContent(email_text, excel_attachment)
        except PermissionDenied:
            return ReportContent(
                _("We are sorry, but your saved report '%(config_name)s' "
                  "is no longer accessible because your subscription does "
                  "not allow Custom Reporting. Please talk to your Project "
                  "Administrator about enabling Custom Reports. If you "
                  "want CommCare HQ to stop sending this message, please "
                  "visit %(saved_reports_url)s to remove this "
                  "Emailed Report.") % {
                      'config_name':
                      self.name,
                      'saved_reports_url':
                      absolute_reverse('saved_reports',
                                       args=[mock_request.domain]),
                  },
                None,
            )
        except Http404:
            return ReportContent(
                _("We are sorry, but your saved report '%(config_name)s' "
                  "can not be generated since you do not have the correct permissions. "
                  "Please talk to your Project Administrator about getting permissions for this"
                  "report.") % {
                      'config_name': self.name,
                  },
                None,
            )
        except UnsupportedSavedReportError:
            return ReportContent(
                _("We are sorry, but your saved report '%(config_name)s' "
                  "is no longer available. If you think this is a mistake, please report an issue."
                  ) % {
                      'config_name': self.name,
                  },
                None,
            )

    @property
    def is_active(self):
        """
        Returns True if the report has a start_date that is in the past or there is
        no start date
        :return: boolean
        """
        return self.start_date is None or self.start_date <= datetime.today(
        ).date()

    @property
    def is_configurable_report(self):
        from corehq.apps.userreports.reports.view import ConfigurableReportView
        return self.report_type == ConfigurableReportView.prefix

    @property
    def supports_translations(self):
        if self.report_type == CustomProjectReportDispatcher.prefix:
            return self.report.get_supports_translations()
        else:
            return self.is_configurable_report

    @property
    @memoized
    def languages(self):
        if self.is_configurable_report:
            return frozenset(self.report.spec.get_languages())
        elif self.supports_translations:
            return frozenset(self.report.languages)
        return frozenset()

    @property
    @memoized
    def configurable_report(self):
        from corehq.apps.userreports.reports.view import ConfigurableReportView
        return ConfigurableReportView.get_report(self.domain, self.report_slug,
                                                 self.subreport_slug)

    @property
    def datespan_filters(self):
        return (self.configurable_report.datespan_filters
                if self.is_configurable_report else [])

    @property
    def has_ucr_datespan(self):
        return self.is_configurable_report and self.datespan_filters

    @staticmethod
    def datespan_filter_choices(datespan_filters, lang):
        localized_datespan_filters = []
        for f in datespan_filters:
            copy = dict(f)
            copy['display'] = ucr_localize(copy['display'], lang)
            localized_datespan_filters.append(copy)

        with localize(lang):
            return [{
                'display': _('Choose a date filter...'),
                'slug': None,
            }] + localized_datespan_filters
예제 #4
0
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.")
예제 #5
0
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.")
예제 #6
0
class LegacyWeeklyReport(Document):
    """
    This doc stores the aggregate weekly results per site.
    Example:
        domain: 'mikesproject',
        site: 'Pennsylvania State Elementary School',
        week_end_date: Saturday Sept 28, 2013,
        site_strategy: [3, -1, 0, 4, 2],
        site_game: [2, 4, 3, 1, 0],
        individual: {
            'mikeo': {
                'strategy': [2, 4, 0, 1, 3],
                'game': [1, 2, 4, 1, 0],
                'weekly_totals': [
                    ['Sept 9', 3],
                    ['Sept 16', 2],
                    ['Sept 23', 5],   # current week
                ],
            },
        },
        'weekly_totals': [
            ['Sept 9', 11],
            ['Sept 16', 6],
            ['Sept 23', 9],   # current week
        ],
    Where each week is a 5 element list.  0 indicates that
    no strategies/games were recorded, -1 indicates an off
    day (nothing recorded, but that's okay).
    """
    domain = StringProperty()
    site = StringProperty()
    week_end_date = DateProperty()
    site_strategy = ListProperty()
    site_game = ListProperty()
    individual = DictProperty()
    weekly_totals = ListProperty()

    @classmethod
    def by_site(cls, site, date=None):
        if isinstance(site, Group):
            site = site.name
        if date is None:
            # get the most recent saturday (isoweekday==6)
            days = [6, 7, 1, 2, 3, 4, 5]
            today = datetime.date.today()
            date = today - datetime.timedelta(
                days=days.index(today.isoweekday()))

        report = cls.view(
            'penn_state/smiley_weekly_reports',
            key=[DOMAIN, site, str(date)],
            reduce=False,
            include_docs=True,
        ).first()
        return report

    @classmethod
    def by_user(cls, user, date=None):
        # Users should only have one group, and it should be a report group
        groups = Group.by_user(user).all()
        # if len(groups) != 1 or not groups[0].reporting:
        if len(groups) == 0 or not groups[0].reporting:
            return

        site = groups[0].name
        return cls.by_site(site, date)
예제 #7
0
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)))
예제 #8
0
class VscanUpload(Document):
    scanner_serial = StringProperty()
    scan_id = StringProperty()
    date = DateProperty()