"""
resourceview.py

Contains administrative views for working with resources.
"""
from datetime import date, datetime

from admin_helpers import *

from sqlalchemy import or_, not_, and_, func
from flask import current_app, redirect, flash, request, url_for
from flask.ext.admin import BaseView, expose
from flask.ext.admin.form import rules
from flask.ext.admin.helpers import get_redirect_target
from flask.ext.admin.actions import action
from flask.ext.admin.contrib.sqla import ModelView
from flask.ext.admin.contrib.sqla.filters import FilterEmpty
from wtforms import DecimalField, validators

import geopy
from geopy.exc import *

from remedy.remedyblueprint import group_active_populations, \
    group_active_categories
from remedy.rad.models import Resource, Category, Population
from remedy.rad.geocoder import Geocoder
from remedy.rad.nullablebooleanfield import NullableBooleanField
from remedy.rad.plaintextfield import PlainTextField
from remedy.rad.statichtmlfield import StaticHtmlField


def scaffold_resource_form(form_class):
    """
    Scaffolds the provided resource form class by ensuring
    location fields are optional and that nullable flag fields
    are actually handled as nullable.

    Args:
        form_class: The form class to update.
        column_labels: The column labels to use.

    Returns:
        The updated form class.
    """
    # Override the latitude/longitude fields to be optional
    form_class.latitude = DecimalField(validators=[validators.Optional()])
    form_class.longitude = DecimalField(validators=[validators.Optional()])

    # Override the nullable flag fields to actually be nullable -
    # otherwise, Flask-Admin treats them as standard Boolean fields
    # (which is bad - we want the N/A option)
    form_class.is_wpath = NullableBooleanField(
        label=resource_column_labels['is_wpath'])
    form_class.is_icath = NullableBooleanField(
        label=resource_column_labels['is_icath'])
    form_class.is_accessible = NullableBooleanField(
        label=resource_column_labels['is_accessible'])
    form_class.has_sliding_scale = NullableBooleanField(
        label=resource_column_labels['has_sliding_scale'])

    return form_class


class ResourceView(AdminAuthMixin, ModelView):
    """
    An administrative view for working with resources.
    """
    can_view_details = True

    column_details_exclude_list = (
        'latitude',
        'longitude',
        'location',
        'category_text',
        'overall_aggregate'
    )

    # Allow exporting
    can_export = True
    export_max_rows = 0
    column_export_list = (
        'name',
        'organization',
        'address',
        'url',
        'email',
        'phone',
        'fax',
        'hours',
        'hospital_affiliation',
        'description',
        'npi',
        'categories',
        'populations',
        'is_icath',
        'is_wpath',
        'is_accessible',
        'has_sliding_scale',
        'visible',
        'advisory_notes',
        'is_approved',
        'submitted_user',
        'submitted_date',
        'submitted_ip',
        'source',
        'notes',
        'date_created',
        'last_updated',
        'date_verified',
        'id'
    )
    column_formatters_export = resource_export_formatters

    column_list = (
        'name',
        'organization',
        'address',
        'url',
        'source',
        'last_updated'
    )

    column_default_sort = 'name'

    column_searchable_list = (
        'name',
        'description',
        'organization',
        'notes',
        'advisory_notes',
    )

    # By default, Flask-Admin isn't going to pick up on the fact
    # that our flags are nullable. Therefore, we need to manually
    # add FilterEmpty options. These use names identical to the
    # column labels for the normal filters so that they are
    # appropriately grouped.
    column_filters = (
        'visible',
        'is_approved',
        'source',
        'npi',
        'date_created',
        'last_updated',
        'date_verified',
        FilterEmpty(Resource.is_icath, 'Informed Consent/ICATH'),
        'is_icath',
        FilterEmpty(Resource.is_wpath, 'WPATH'),
        'is_wpath',
        FilterEmpty(Resource.is_accessible, 'ADA/Wheelchair Accessible'),
        'is_accessible',
        FilterEmpty(Resource.has_sliding_scale, 'Sliding Scale'),
        'has_sliding_scale',
        'advisory_notes',
    )

    form_excluded_columns = (
        'date_created',
        'last_updated',
        'category_text',
        'reviews',
        'aggregateratings',
        'submitted_user',
        'submitted_ip',
        'submitted_date',
        'is_approved',
        'overall_aggregate'
    )

    form_rules = [
        rules.FieldSet((
            rules.HTML('<hr />'),
            'name',
            'organization',
            'description',
            'categories',
            'populations',
            'visible',
            rules.HTML('<hr />')),
            'Basic Information'),

        rules.FieldSet((
            rules.HTML('<hr />'),
            'address',
            'latitude',
            'longitude',
            'location',
            'phone',
            'fax',
            'email',
            'url',
            'hours',
            rules.HTML('<hr />')),
            'Contact Information'),

        rules.FieldSet((
            rules.HTML('<hr />'),
            'hospital_affiliation',
            'is_icath',
            'is_wpath',
            'is_accessible',
            'has_sliding_scale',
            rules.HTML('<hr />')),
            'Practice Information'),

        rules.FieldSet((
            rules.HTML('<hr />'),
            'npi',
            'date_verified',
            'advisory_notes',
            'notes',
            'source'),
            'Other')
    ]

    create_template = 'admin/resource_create.html'

    edit_template = 'admin/resource_edit.html'

    # Use standard labels/descriptions/formatters
    column_labels = resource_column_labels
    column_descriptions = resource_column_descriptions
    column_formatters = resource_column_formatters

    def edit_form(self, obj=None):
        """
        Overrides the editing form to disable toggling
        active status on unapproved resources.
        """
        form = super(ResourceView, self).edit_form(obj)

        # HACK: Indicate we shouldn't change the "Visible" field
        # if we're attempting to edit an unapproved resource.
        if obj is not None and not obj.is_approved:
            form.visible.description = \
                'Visibility should not be changed for unapproved resources.'

        return form

    def scaffold_form(self):
        """
        Scaffolds the creation/editing form so that the latitude
        and longitude fields are optional, but can still be set
        by the Google Places API integration.
        """
        form_class = super(ResourceView, self).scaffold_form()

        # Scaffold our default stuff
        form_class = scaffold_resource_form(form_class)

        return form_class

    def on_model_change(self, form, model, is_created):
        """
        Updates the last_updated date on the provided model
        if is_created is false.
        """
        if not is_created:
            model.last_updated = datetime.utcnow()

    @action(
        'togglevisible',
        'Toggle Visibility',
        'Are you sure you wish to toggle visibility ' +
        'for the selected resources?')
    def action_togglevisible(self, ids):
        """
        Attempts to toggle visibility for each of the specified resources.

        Args:
            ids: The list of resource IDs, indicating which resources
                should have their visibility toggled.
        """
        # Load all resources by the set of IDs - also, only
        # allow this for approved resources
        target_resources = self.get_query(). \
            filter(self.model.id.in_(ids)). \
            filter(self.model.is_approved == True). \
            all()

        # Build a list of all the results
        results = []

        if len(target_resources) > 0:

            for resource in target_resources:
                # Build a helpful message string to use for messages.
                resource_str = 'resource #' + str(resource.id) + \
                    ' (' + resource.name + ')'
                visible_status = ''
                try:

                    if not resource.visible:
                        resource.visible = True
                        visible_status = ' as visible'
                    else:
                        resource.visible = False
                        visible_status = ' as not visible'

                    resource.last_updated = datetime.utcnow()
                except Exception as ex:
                    results.append(
                        'Error changing ' + resource_str + ': ' + str(ex))
                else:
                    results.append(
                        'Marked ' + resource_str + visible_status + '.')

            # Save our changes.
            self.session.commit()

        else:
            results.append('No resources were selected.')

        # Flash the results of everything
        flash("\n".join(msg for msg in results))

    @action(
        'markverified',
        'Mark Verified',
        'Are you sure you wish to mark the selected resources as verified?')
    def action_markverified(self, ids):
        """
        Attempts to mark each of the specified resources as verified
        on the current date.

        Args:
            ids: The list of resource IDs, indicating which resources
                should be marked as verified.
        """
        # Load all resources by the set of IDs
        target_resources = self.get_query(). \
            filter(self.model.id.in_(ids)).all()

        # Build a list of all the results
        results = []

        if len(target_resources) > 0:

            for resource in target_resources:
                # Build a helpful message string to use for messages.
                resource_str = 'resource #' + str(resource.id) + \
                    ' (' + resource.name + ')'
                try:
                    resource.date_verified = date.today()
                    resource.last_updated = datetime.utcnow()
                except Exception as ex:
                    results.append(
                        'Error changing ' + resource_str + ': ' + str(ex))
                else:
                    results.append(
                        'Marked ' + resource_str + ' as verified.')

            # Save our changes.
            self.session.commit()

        else:
            results.append('No resources were selected.')

        # Flash the results of everything
        flash("\n".join(msg for msg in results))

    @action('assigncategories', 'Assign Categories')
    def action_assigncategories(self, ids):
        """
        Sets up a redirection action for mass-assigning categories
        to the specified resources.

        Args:
            ids: The list of resource IDs that should be updated.
        """
        return_url = get_redirect_target() or self.get_url('.index_view')

        return redirect(
            self.get_url(
                'resourcecategoryassignview.index',
                url=return_url,
                ids=ids))

    @action('assignpopulations', 'Assign Populations')
    def action_assignpopulations(self, ids):
        """
        Sets up a redirection action for mass-assigning populations
        to the specified resources.

        Args:
            ids: The list of resource IDs that should be updated.
        """
        return_url = get_redirect_target() or self.get_url('.index_view')

        return redirect(
            self.get_url(
                'resourcepopulationassignview.index',
                url=return_url,
                ids=ids))

    def __init__(self, session, **kwargs):
        super(ResourceView, self).__init__(Resource, session, **kwargs)


class ResourceRequiringGeocodingView(ResourceView):
    """
    An administrative view for working with resources that need geocoding.
    """
    column_list = ('name', 'organization', 'address', 'source')

    # Disable model creation/deletion
    can_create = False
    can_delete = False

    def get_query(self):
        """
        Returns the query for the model type.

        Returns:
            The query for the model type.
        """
        query = self.session.query(self.model)
        return self.prepare_geocode_query(query)

    def get_count_query(self):
        """
        Returns the count query for the model type.

        Returns:
            The count query for the model type.
        """
        query = self.session.query(func.count('*')).select_from(self.model)
        return self.prepare_geocode_query(query)

    def prepare_geocode_query(self, query):
        """
        Prepares the provided query by ensuring that
        all relevant geocoding-related filters have been applied.

        Args:
            query: The query to update.

        Returns:
            The updated query.
        """
        # Ensure an address is defined
        query = query.filter(self.model.address != None)
        query = query.filter(self.model.address != '')

        # Ensure at least one geocoding field is missing
        query = query.filter(or_(
            self.model.latitude == None,
            self.model.longitude == None))

        return query

    @action(
        'geocode',
        'Geocode')
    def action_geocode(self, ids):
        """
        Attempts to geocode each of the specified resources.

        Args:
            ids: The list of resource IDs, indicating which resources
                should be geocoded.
        """
        # Load all resources by the set of IDs
        target_resources = self.get_query(). \
            filter(self.model.id.in_(ids)).all()

        # Build a list of all the results
        results = []

        if len(target_resources) > 0:

            # Set up the geocoder, and then try to geocode each resource
            geocoder = Geocoder(
                api_key=current_app.config.get('MAPS_SERVER_KEY'))

            for resource in target_resources:
                # Build a helpful message string to use for errors.
                resource_str = 'resource #' + str(resource.id) + \
                    ' (' + resource.name + ')'
                try:
                    geocoder.geocode(resource)
                    resource.last_updated = datetime.utcnow()
                except geopy.exc.GeopyError as gpex:
                    # Handle Geopy errors separately
                    exc_type = ''

                    # Attempt to infer some extra information based
                    # on the exception type
                    if isinstance(
                            gpex,
                            geopy.exc.GeocoderQuotaExceeded):
                        exc_type = 'quota exceeded'
                    elif isinstance(
                            gpex,
                            geopy.exc.GeocoderAuthenticationFailure):
                        exc_type = 'authentication failure'
                    elif isinstance(
                            gpex,
                            geopy.exc.GeocoderInsufficientPrivileges):
                        exc_type = 'insufficient privileges'
                    elif isinstance(
                            gpex,
                            geopy.exc.GeocoderUnavailable):
                        exc_type = 'server unavailable'
                    elif isinstance(
                            gpex,
                            geopy.exc.GeocoderTimedOut):
                        exc_type = 'timed out'
                    elif isinstance(
                            gpex,
                            geopy.exc.GeocoderQueryError):
                        exc_type = 'query error'

                    if len(exc_type) > 0:
                        exc_type = '(' + exc_type + ') '

                    results.append(
                        'Error geocoding ' + resource_str + ': ' +
                        exc_type + str(gpex))
                except Exception as ex:
                    results.append(
                        'Error geocoding ' + resource_str + ': ' + str(ex))
                else:
                    results.append('Geocoded ' + resource_str + '.')

            # Save our changes.
            self.session.commit()

        else:
            results.append('No resources were selected.')

        # Flash the results of everything
        flash("\n".join(msg for msg in results))

    @action(
        'removeaddress',
        'Remove Address',
        'Are you sure you wish to remove address information ' +
        'from the selected resources?')
    def action_remove_address(self, ids):
        """
        Attempts to remove address information from each of the
        specified resources.

        Args:
            ids: The list of resource IDs, indicating which resources
                should have address information stripped.
        """
        # Load all resources by the set of IDs
        target_resources = self.get_query(). \
            filter(self.model.id.in_(ids)).all()

        # Build a list of all the results
        results = []

        if len(target_resources) > 0:
            for resource in target_resources:
                # Build a helpful message string to use for errors.
                resource_str = 'resource #' + str(resource.id) + \
                    ' (' + resource.name + ')'
                try:
                    resource.address = None
                    resource.latitude = None
                    resource.longitude = None
                    resource.location = None
                    resource.last_updated = datetime.utcnow()
                except Exception as ex:
                    results.append(
                        'Error updating ' + resource_str + ': ' + str(ex))
                else:
                    results.append(
                        'Removed address information from ' + resource_str +
                        '.')

            # Save our changes.
            self.session.commit()
        else:
            results.append('No resources were selected.')

        # Flash the results of everything
        flash("\n".join(msg for msg in results))

    def __init__(self, session, **kwargs):
        # Because we're invoking the ResourceView constructor,
        # we don't need to pass in the ResourceModel.
        super(ResourceRequiringGeocodingView, self).__init__(
            session,
            **kwargs)


class ResourceRequiringCategoriesView(ResourceView):
    """
    An administrative view for working with resources that need categories.
    """
    column_list = ('name', 'organization', 'address', 'source')

    # Disable model creation/deletion
    can_create = False
    can_delete = False

    def get_query(self):
        """
        Returns the query for the model type.

        Returns:
            The query for the model type.
        """
        query = self.session.query(self.model)
        return self.prepare_category_query(query)

    def get_count_query(self):
        """
        Returns the count query for the model type.

        Returns:
            The count query for the model type.
        """
        query = self.session.query(func.count('*')).select_from(self.model)
        return self.prepare_category_query(query)

    def prepare_category_query(self, query):
        """
        Prepares the provided query by ensuring that
        filtering out resources with categories has been applied.

        Args:
            query: The query to update.

        Returns:
            The updated query.
        """
        # Ensure there are no categories defined
        query = query.filter(not_(self.model.categories.any()))

        return query

    def __init__(self, session, **kwargs):
        # Because we're invoking the ResourceView constructor,
        # we don't need to pass in the ResourceModel.
        super(ResourceRequiringCategoriesView, self).__init__(
            session,
            **kwargs)


class ResourceRequiringPopulationsView(ResourceView):
    """
    An administrative view for working with resources that need populations.
    """
    column_list = ('name', 'organization', 'address', 'source')

    # Disable model creation/deletion
    can_create = False
    can_delete = False

    def get_query(self):
        """
        Returns the query for the model type.

        Returns:
            The query for the model type.
        """
        query = self.session.query(self.model)
        return self.prepare_population_query(query)

    def get_count_query(self):
        """
        Returns the count query for the model type.

        Returns:
            The count query for the model type.
        """
        query = self.session.query(func.count('*')).select_from(self.model)
        return self.prepare_population_query(query)

    def prepare_population_query(self, query):
        """
        Prepares the provided query by ensuring that
        filtering out resources with populations has been applied.

        Args:
            query: The query to update.

        Returns:
            The updated query.
        """
        # Ensure there are no populations defined
        query = query.filter(not_(self.model.populations.any()))

        return query

    def __init__(self, session, **kwargs):
        # Because we're invoking the ResourceView constructor,
        # we don't need to pass in the ResourceModel.
        super(ResourceRequiringPopulationsView, self).__init__(
            session,
            **kwargs)


class ResourceCategoryAssignView(AdminAuthMixin, BaseView):
    """
    The view for mass-assigning resources to categories.
    """
    # Not visible in the menu.
    def is_visible(self):
        return False

    @expose('/', methods=['GET', 'POST'])
    def index(self):
        """
        A view for mass-assigning resources to categories.
        """
        return_url = get_redirect_target() or \
            self.get_url('category-resourceview.index_view')

        # Load all resources by the set of IDs
        target_resources = Resource.query.filter(
            Resource.id.in_(request.args.getlist('ids')))

        target_resources = target_resources. \
            order_by(Resource.name.asc()).all()

        # Make sure we have some, and go back to the resources
        # view (for assigning categories) if we don't.
        if len(target_resources) == 0:
            flash('At least one resource must be selected.', 'error')
            return redirect(url_for(return_url))

        if request.method == 'GET':
            # Get all categories
            available_categories = Category.query. \
                order_by(Category.name.asc()).all()

            # Group them using the remedyblueprint method
            grouped_categories = group_active_categories(
                available_categories)

            # Return the view for assigning categories
            return self.render(
                'admin/resource_assign_categories.html',
                ids=request.args.getlist('ids'),
                resources=target_resources,
                grouped_categories=grouped_categories,
                return_url=return_url)
        else:
            # Get the selected categories - use request.form,
            # not request.args
            target_categories = Category.query.filter(
                Category.id.in_(request.form.getlist('categories'))).all()

            if len(target_categories) > 0:
                # Build a list of all the results
                results = []

                for resource in target_resources:
                    # Build a helpful message string to use for resources.
                    resource_str = 'resource #' + str(resource.id) + \
                        ' (' + resource.name + ')'

                    try:
                        # Assign all categories
                        for category in target_categories:

                            # Make sure we're not double-adding
                            if category not in resource.categories:
                                resource.categories.append(category)
                                resource.last_updated = datetime.utcnow()

                    except Exception as ex:
                        results.append(
                            'Error updating ' + resource_str + ': ' + str(ex))
                    else:
                        results.append(
                            'Updated ' + resource_str + '.')

                # Save our changes.
                self.session.commit()

                # Flash the results of everything
                flash("\n".join(msg for msg in results))
            else:
                flash('At least one category must be selected.', 'error')

            return redirect(return_url)

    def __init__(self, session, **kwargs):
        self.session = session
        super(ResourceCategoryAssignView, self).__init__(**kwargs)


class ResourcePopulationAssignView(AdminAuthMixin, BaseView):
    """
    The view for mass-assigning resources to populations.
    """
    # Not visible in the menu.
    def is_visible(self):
        return False

    @expose('/', methods=['GET', 'POST'])
    def index(self):
        """
        A view for mass-assigning resources to populations.
        """
        return_url = get_redirect_target() or \
            self.get_url('population-resourceview.index_view')

        # Load all resources by the set of IDs
        target_resources = Resource.query. \
            filter(Resource.id.in_(request.args.getlist('ids')))

        target_resources = target_resources. \
            order_by(Resource.name.asc()).all()

        # Make sure we have some, and go back to the resources
        # view (for assigning populations) if we don't.
        if len(target_resources) == 0:
            flash('At least one resource must be selected.', 'error')
            return redirect(url_for(return_url))

        if request.method == 'GET':
            # Get all populations
            available_populations = Population.query. \
                order_by(Population.name.asc()).all()

            # Group them using the remedyblueprint method
            grouped_populations = group_active_populations(
                available_populations)

            # Return the view for assigning populations
            return self.render(
                'admin/resource_assign_populations.html',
                ids=request.args.getlist('ids'),
                resources=target_resources,
                grouped_populations=grouped_populations,
                return_url=return_url)
        else:
            # Get the selected populations - use request.form,
            # not request.args
            target_populations = Population.query.filter(
                Population.id.in_(request.form.getlist('populations'))).all()

            if len(target_populations) > 0:
                # Build a list of all the results
                results = []

                for resource in target_resources:
                    # Build a helpful message string to use for resources.
                    resource_str = 'resource #' + str(resource.id) + \
                        ' (' + resource.name + ')'

                    try:
                        # Assign all populations
                        for population in target_populations:

                            # Make sure we're not double-adding
                            if population not in resource.populations:
                                resource.populations.append(population)
                                resource.last_updated = datetime.utcnow()

                    except Exception as ex:
                        results.append(
                            'Error updating ' + resource_str + ': ' + str(ex))
                    else:
                        results.append(
                            'Updated ' + resource_str + '.')

                # Save our changes.
                self.session.commit()

                # Flash the results of everything
                flash("\n".join(msg for msg in results))
            else:
                flash('At least one population must be selected.', 'error')

            return redirect(return_url)

    def __init__(self, session, **kwargs):
        self.session = session
        super(ResourcePopulationAssignView, self).__init__(**kwargs)


class ResourceRequiringNpiView(ResourceView):
    """
    An administrative view for working with resources that need NPI values.
    """
    # Disable model creation/deletion
    can_create = False
    can_delete = False

    def get_query(self):
        """
        Returns the query for the model type.

        Returns:
            The query for the model type.
        """
        query = self.session.query(self.model)
        return self.prepare_npi_query(query)

    def get_count_query(self):
        """
        Returns the count query for the model type.

        Returns:
            The count query for the model type.
        """
        query = self.session.query(func.count('*')).select_from(self.model)
        return self.prepare_npi_query(query)

    def prepare_npi_query(self, query):
        """
        Prepares the provided query by ensuring that
        filtering out resources with NPIs has been applied.

        Args:
            query: The query to update.

        Returns:
            The updated query.
        """
        # Ensure that an NPI is missing
        query = query.filter(or_(
            self.model.npi == None,
            self.model.npi == ''))

        return query

    def __init__(self, session, **kwargs):
        # Because we're invoking the ResourceView constructor,
        # we don't need to pass in the ResourceModel.
        super(ResourceRequiringNpiView, self).__init__(session, **kwargs)


class SubmittedResourceView(AdminAuthMixin, ModelView):
    """
    An administrative view for working with submitted resources
    pending administrator approval.
    """
    can_view_details = True

    column_details_exclude_list = (
        'latitude',
        'longitude',
        'location',
        'category_text',
        'is_approved',
        'visible',
        'date_verified',
        'overall_aggregate'
    )

    # Disable model creation
    can_create = False

    # Allow exporting
    can_export = True
    export_max_rows = 0
    column_export_list = (
        'name',
        'organization',
        'address',
        'url',
        'email',
        'phone',
        'fax',
        'hours',
        'hospital_affiliation',
        'description',
        'npi',
        'categories',
        'populations',
        'is_icath',
        'is_wpath',
        'is_accessible',
        'has_sliding_scale',
        'submitted_user',
        'submitted_date',
        'submitted_ip',
        'notes',
        'advisory_notes',
        'date_created',
        'last_updated',
        'id'
    )
    column_formatters_export = resource_export_formatters

    column_list = (
        'name',
        'organization',
        'address',
        'url',
        'submitted_user',
        'submitted_date'
    )

    column_default_sort = 'submitted_date'

    column_sortable_list = (
        'name',
        'organization',
        'submitted_date',
        'address',
        'url',
        ('submitted_user', 'submitted_user.username')
    )

    column_searchable_list = (
        'name',
        'description',
        'organization',
        'notes',
        'advisory_notes',
    )

    column_filters = (
        'submitted_date',
    )

    form_excluded_columns = (
        'date_created',
        'last_updated',
        'category_text',
        'reviews',
        'aggregateratings',
        'submitted_user',
        'submitted_ip',
        'submitted_date',
        'is_approved',
        'visible',
        'source',
        'overall_aggregate'
    )

    edit_template = 'admin/submitted_resource_edit.html'

    # Use standard labels/descriptions/formatters
    column_labels = resource_column_labels
    column_descriptions = resource_column_descriptions
    column_formatters = resource_column_formatters

    form_extra_fields = {
        'potential_dupes': StaticHtmlField('Detected'),
        'submitted_user_text': StaticHtmlField(
            resource_column_labels['submitted_user']),
        'submitted_ip_text': PlainTextField(
            resource_column_labels['submitted_ip']),
        'submitted_date_text': PlainTextField(
            resource_column_labels['submitted_date']),
        'review_rating': PlainTextField(
            review_column_labels['rating']),
        'review_staff_rating': PlainTextField(
            review_column_labels['staff_rating']),
        'review_intake_rating': PlainTextField(
            review_column_labels['intake_rating']),
        'review_text': PlainTextField(
            review_column_labels['text'])
    }

    form_rules = [
        rules.FieldSet((
            rules.HTML('<hr />'),
            'potential_dupes',
            rules.HTML('<hr />')),
            'Potential Duplicates'),

        rules.FieldSet((
            rules.HTML('<hr />'),
            'name',
            'organization',
            'description',
            'categories',
            'populations',
            rules.HTML('<hr />')),
            'Basic Information'),

        rules.FieldSet((
            rules.HTML('<hr />'),
            'address',
            'latitude',
            'longitude',
            'location',
            'phone',
            'fax',
            'email',
            'url',
            'hours',
            rules.HTML('<hr />')),
            'Contact Information'),

        rules.FieldSet((
            rules.HTML('<hr />'),
            'hospital_affiliation',
            'is_icath',
            'is_wpath',
            'is_accessible',
            'has_sliding_scale',
            rules.HTML('<hr />')),
            'Practice Information'),

        rules.FieldSet((
            rules.HTML('<hr />'),
            'npi',
            'date_verified',
            'advisory_notes',
            'notes',
            rules.HTML('<hr />')),
            'Other'),

        rules.FieldSet((
            rules.HTML('<hr />'),
            'submitted_user_text',
            'submitted_ip_text',
            'submitted_date_text',
            rules.HTML('<hr />')),
            'Submission Information'),

        rules.FieldSet((
            rules.HTML('<hr />'),
            'review_rating',
            'review_staff_rating',
            'review_intake_rating',
            'review_text'),
            'Review')
    ]

    def get_query(self):
        """
        Returns the query for the model type.

        Returns:
            The query for the model type.
        """
        query = self.session.query(self.model)
        return self.prepare_submitted_query(query)

    def get_count_query(self):
        """
        Returns the count query for the model type.

        Returns:
            The count query for the model type.
        """
        query = self.session.query(func.count('*')).select_from(self.model)
        return self.prepare_submitted_query(query)

    def prepare_submitted_query(self, query):
        """
        Prepares the provided query by ensuring that
        only resources pending approval are included.

        Args:
            query: The query to update.

        Returns:
            The updated query.
        """
        # Ensure a submission IP is defined
        query = query.filter(self.model.submitted_ip.isnot(None))
        query = query.filter(self.model.submitted_ip != '')

        # Ensure that we're marked as visible and unapproved
        query = query.filter(self.model.visible == True)
        query = query.filter(self.model.is_approved == False)

        return query

    def edit_form(self, obj=None):
        """
        Overrides the editing form to include additional
        read-only plain text and HTML fields regarding
        the submitted resource.
        """
        form = super(SubmittedResourceView, self).edit_form(obj)

        # Try to detect duplicates based on matching names/NPIs
        dup_resources = self.session.query(Resource). \
            filter(Resource.id != obj.id). \
            filter(or_(
                and_(
                    Resource.npi != '',
                    Resource.npi.isnot(None),
                    Resource.npi == obj.npi),
                Resource.name == obj.name.strip())). \
            all()

        if len(dup_resources) > 0:
            # Build a list of potential duplicates with a link -
            # make sure we're escaping each item.
            form.potential_dupes.default = '<br />'.join(
                [get_resource_link(r) + ' (ID: ' + str(r.id) + ')'
                    for r in dup_resources])
        else:
            form.potential_dupes.default = 'None'

        # Add read-only submission fields
        if obj.submitted_user is not None:
            # Get fancy and have a user link and email
            user_text = get_user_link(obj.submitted_user) + ' - ' + \
                get_email_link(
                    obj.submitted_user,
                    'Provider Submission - ' + obj.name)

            form.submitted_user_text.default = user_text
        else:
            form.submitted_user_text.default = 'Deleted User'

        form.submitted_ip_text.default = obj.submitted_ip
        form.submitted_date_text.default = obj.submitted_date

        # Add review fields
        review = obj.reviews.first()

        if review is not None:
            form.review_text.default = review.text
            form.review_rating.default = review.rating
            form.review_intake_rating.default = review.intake_rating
            form.review_staff_rating.default = review.staff_rating

        return form

    def scaffold_form(self):
        """
        Scaffolds the creation/editing form so that the latitude
        and longitude fields are optional, but can still be set
        by the Google Places API integration.
        """
        form_class = super(SubmittedResourceView, self).scaffold_form()

        # Scaffold our default stuff
        form_class = scaffold_resource_form(form_class)

        return form_class

    def on_model_change(self, form, model, is_created):
        """
        Ensures that fields are updated in response to specific
        approval/rejection actions.

        Also updates the last_updated date on the provided model
        if is_created is false.
        """
        if not is_created:
            model.last_updated = datetime.utcnow()

        # If we're approving, mark the resource as approved
        # and update the verified date.
        # If we're rejecting, mark the resource as hidden.
        if '_approve_resource' in request.form:
            model.is_approved = True
            model.date_verified = date.today()
        elif '_reject_resource' in request.form:
            model.visible = False

    def __init__(self, session, **kwargs):
        super(SubmittedResourceView, self).__init__(
            Resource,
            session,
            **kwargs)