<commit_before>"""Patient API - implements patient specific views such as patient search

NB - this is not to be confused with 'patients', which defines views
for staff

"""
from datetime import datetime
import json

from flask import Blueprint, abort, jsonify, request
from sqlalchemy import and_
from werkzeug.exceptions import Unauthorized

from ..audit import auditable_event
from ..database import db
from ..extensions import oauth
from ..models.fhir import bundle_results
from ..models.identifier import (
    Identifier,
    UserIdentifier,
    parse_identifier_params,
)
from ..models.overall_status import OverallStatus
from ..models.questionnaire_bank import QuestionnaireBank
from ..models.questionnaire_response import QuestionnaireResponse
from ..models.qb_timeline import QBT, update_users_QBT
from ..models.reference import Reference
from ..models.role import ROLE
from ..models.user import User, current_user, get_user_or_abort
from .crossdomain import crossdomain
from .demographics import demographics

patient_api = Blueprint('patient_api', __name__)

@patient_api.route('/api/patient/')
@crossdomain()
@oauth.require_oauth()
def patient_search():
    """Looks up patient(s) from given parameters, returns FHIR Patient bundle

    Takes key=value pairs to look up.  Email, identifier searches supported.

    Example searches:
        /api/patient?email=username@server.com
        /api/patient?identifier=http://us.truenth.org/identity-codes/external-study-id|123-45-678

    Identifier search pattern:
        ?identifier=<system>|<value>

    Deleted users and users for which the authenticated user does not have
    permission to view will NOT be included in the results.

    NB - the results are restricted to users with the patient role.  It is
    therefore possible to get no results from this and still see a unique email
    collision from existing non-patient users.

    NB - currently out of FHIR DSTU2 spec by default.  Include query string
    parameter ``patch_dstu2=True`` to properly return a FHIR bundle resource
    (https://www.hl7.org/fhir/DSTU2/bundle.html) naming the ``total`` matches
    and references to all matching patients.  With ``patch_dstu2=True``, the
    total will be zero if no matches are found, whereas the default (old,
    non-compliant) behavior is to return a 404 when no match is found.
    Please consider using the ``patch_dstu2=True`` parameter, as this will
    become the default behavior in the future.

    Returns a FHIR bundle resource (https://www.hl7.org/fhir/DSTU2/bundle.html)
    formatted in JSON for all matching valid, accessible patients, with
    ``patch_dstu2=True`` set (preferred).  Default returns single patient on a
    match, 404 on no match, and 400 for multiple as it's not supported.

    ---
    tags:
      - Patient
    operationId: patient_search
    produces:
      - application/json
    parameters:
      - name: search_parameters
        in: query
        description:
            Search parameters, such as `email` or `identifier`.  For
            identifier, URL-encode the `system` and `value` using '|' (pipe)
            delimiter, i.e. `api/patient/?identifier=http://fake.org/id|12a7`
        required: true
        type: string
      - name: patch_dstu2
        in: query
        description: whether or not to return DSTU2 compliant bundle
        required: false
        type: boolean
        default: false
    responses:
      200:
        description:
          Returns FHIR patient resource
          (https://www.hl7.org/fhir/DSTU2/patient.html) in JSON if a match
          is found.  Otherwise responds with a 404 status code.
      401:
        description:
          if missing valid OAuth token
      404:
        description:
          if there is no match found, or the user lacks permission to look
          up details on the match.
    security:
      - ServiceToken: []

    """
    if not request.args.items():
        abort(400, "missing search criteria")

    query = User.query.filter(User.deleted_id.is_(None))
    for k, v in request.args.items():
        if k == 'email':
            query = query.filter(User.email == v)
        elif k == 'identifier':
            system, value = parse_identifier_params(v)
            if not (system and value):
                abort(400, "need 'system' and 'value' to look up identifier")

            query = query.join(UserIdentifier).join(
                Identifier).filter(and_(
                    UserIdentifier.identifier_id == Identifier.id,
                    Identifier.system == system,
                    Identifier._value == value))
        elif k == 'patch_dstu2':
            # not search criteria, but valid
            continue
        else:
            abort(400, "can't search on '{}' at this time".format(k))

    # Validate permissions to see every requested user - omitting those w/o
    patients = []
    for user in query:
        try:
            current_user().check_role(permission='view', other_id=user.id)
            if user.has_role(ROLE.PATIENT.value):
                patients.append(
                    {'resource': Reference.patient(user.id).as_fhir()})
        except Unauthorized:
            # Mask unauthorized as a not-found.  Don't want unauthed users
            # farming information - i.e. don't add to results
            auditable_event("looking up users with inadequate permission",
                            user_id=current_user().id, subject_id=user.id,
                            context='authentication')

    if request.args.get('patch_dstu2', False):
        link = {"rel": "self", "href": request.url}
        return jsonify(bundle_results(elements=patients, links=[link]))
    else:
        # Emulate old results
        if len(patients) == 0:
            abort(404)
        if len(patients) > 1:
            abort(400, "multiple results found, include `patch_dstu2=True`")

        ref = Reference.parse(patients[0]['resource'])
        return demographics(patient_id=ref.id)

@patient_api.route('/api/patient/<int:patient_id>/deceased', methods=('POST',))
@crossdomain()
@oauth.require_oauth()
def post_patient_deceased(patient_id):
    """POST deceased datetime or status for a patient

    This convenience API wraps the ability to set a patient's
    deceased status and deceased date time - generally the /api/demographics
    API should be preferred.

    ---
    operationId: deceased
    tags:
      - Patient
    produces:
      - application/json
    parameters:
      - name: patient_id
        in: path
        description: TrueNTH user ID
        required: true
        type: integer
        format: int64
      - in: body
        name: body
        schema:
          id: deceased_details
          properties:
            deceasedBoolean:
              type: string
              description:
                true or false.  Implicitly true with deceasedDateTime value
            deceasedDateTime:
              type: string
              description: valid FHIR datetime string defining time of death
    responses:
      200:
        description:
          Returns updated [FHIR patient
          resource](http://www.hl7.org/fhir/patient.html) in JSON.
      400:
        description:
          if given parameters don't function, such as a false
          deceasedBoolean AND a deceasedDateTime value.
      401:
        description:
          if missing valid OAuth token or logged-in user lacks permission
          to view requested patient
    security:
      - ServiceToken: []

    """
    current_user().check_role(permission='edit', other_id=patient_id)
    patient = get_user_or_abort(patient_id)
    if not request.json or set(request.json.keys()).isdisjoint(
            {'deceasedDateTime', 'deceasedBoolean'}):
        abort(400, "Requires deceasedDateTime or deceasedBoolean in JSON")

    patient.update_deceased(request.json)
    db.session.commit()
    auditable_event("updated demographics on user {0} from input {1}".format(
        patient.id, json.dumps(request.json)), user_id=current_user().id,
        subject_id=patient.id, context='user')

    return jsonify(patient.as_fhir(include_empties=False))

@patient_api.route(
    '/api/patient/<int:patient_id>/birthdate', methods=('POST',))
@patient_api.route(
    '/api/patient/<int:patient_id>/birthDate', methods=('POST',))
@oauth.require_oauth()
def post_patient_dob(patient_id):
    """POST date of birth for a patient

    This convenience API wraps the ability to set a patient's birthDate.
    Generally the /api/demographics API should be preferred.

    ---
    tags:
      - Patient
    produces:
      - application/json
    parameters:
      - name: patient_id
        in: path
        description: TrueNTH user ID
        required: true
        type: integer
        format: int64
      - in: body
        name: body
        schema:
          id: dob_details
          properties:
            birthDate:
              type: string
              description: valid FHIR date string defining date of birth
    responses:
      200:
        description:
          Returns updated [FHIR patient
          resource](http://www.hl7.org/fhir/patient.html) in JSON.
      400:
        description:
          if given parameters don't validate
      401:
        description:
          if missing valid OAuth token or logged-in user lacks permission
          to edit requested patient
    security:
      - ServiceToken: []

    """
    current_user().check_role(permission='edit', other_id=patient_id)
    patient = get_user_or_abort(patient_id)
    if not request.json or 'birthDate' not in request.json:
        abort(400, "Requires `birthDate` in JSON")

    patient.update_birthdate(request.json)
    db.session.commit()
    auditable_event("updated demographics on user {0} from input {1}".format(
        patient.id, json.dumps(request.json)), user_id=current_user().id,
        subject_id=patient.id, context='user')

    return jsonify(patient.as_fhir(include_empties=False))

@patient_api.route('/api/patient/<int:patient_id>/timeline')
@oauth.require_oauth()
def patient_timeline(patient_id):
    from ..date_tools import FHIR_datetime
    from ..models.qbd import QBD
    from ..models.qb_status import QB_Status
    from ..models.questionnaire_bank import visit_name
    from ..trace import dump_trace, establish_trace

    trace = request.args.get('trace', False)
    if trace:
        establish_trace("BEGIN time line lookup for {}".format(patient_id))

    current_user().check_role(permission='view', other_id=patient_id)

    purge = request.args.get('purge', False)
    try:
        update_users_QBT(patient_id, invalidate_existing=purge)
    except ValueError as ve:
        abort(500, ve.message)

    results = []
    # We order by at (to get the latest status for a given QB) and
    # secondly by id, as on rare occasions, the time (`at`) of
    #  `due` == `completed`, but the row insertion defines priority
    for qbt in QBT.query.filter(QBT.user_id == patient_id).order_by(
            QBT.at, QBT.id):
        # build qbd for visit name
        qbd = QBD(
            relative_start=qbt.at, qb_id=qbt.qb_id,
            iteration=qbt.qb_iteration, recur_id=qbt.qb_recur_id)
        if qbt.status == OverallStatus.withdrawn:
            results.append({
                'status': str(qbt.status),
                'at': FHIR_datetime.as_fhir(qbt.at)})
        else:
            results.append({
                'status': str(qbt.status),
                'at': FHIR_datetime.as_fhir(qbt.at),
                'qb (id, iteration)': "{} ({}, {})".format(
                    qbd.questionnaire_bank.name, qbd.qb_id, qbd.iteration),
                'visit': visit_name(qbd)})

    qb_names = {qb.id: qb.name for qb in QuestionnaireBank.query.all()}

    qnrs = QuestionnaireResponse.query.filter(
        QuestionnaireResponse.subject_id == patient_id).order_by(
        QuestionnaireResponse.authored)
    posted = [{
        'at, qb, iteration, status, name': "{}, {}, {}, {}, {}".format(
            qnr.authored,
            qb_names[qnr.questionnaire_bank_id],
            qnr.qb_iteration,
            qnr.status,
            qnr.document['questionnaire']['reference'].split('/')[-1])
        } for qnr in qnrs]

    qbstatus = QB_Status(
        user=User.query.get(patient_id), as_of_date=datetime.utcnow())
    prev_qbd = qbstatus.prev_qbd
    current = qbstatus.current_qbd()
    next_qbd = qbstatus.next_qbd
    status = {
        'overall': str(qbstatus.overall_status),
        'previous QBD': prev_qbd.as_json() if prev_qbd else None,
        'current QBD': current.as_json() if current else None,
        'next QBD': next_qbd.as_json() if next_qbd else None,
    }
    if current:
        status['due'] = qbstatus.due_date
        status['overdue'] = qbstatus.overdue_date
        status['expired'] = qbstatus.expired_date
        status['needing-full'] = qbstatus.instruments_needing_full_assessment()
        status['in-progress'] = qbstatus.instruments_in_progress()
        status['completed'] = qbstatus.instruments_completed()

    if trace:
        return jsonify(
            status=status,
            posted=posted,
            timeline=results,
            trace=dump_trace("END time line lookup"))
    return jsonify(status=status, posted=posted, timeline=results)
<commit_msg>null safe qb_name lookup, for when qnr doesn't have an assigned qb.<commit_after>"""Patient API - implements patient specific views such as patient search

NB - this is not to be confused with 'patients', which defines views
for staff

"""
from datetime import datetime
import json

from flask import Blueprint, abort, jsonify, request
from sqlalchemy import and_
from werkzeug.exceptions import Unauthorized

from ..audit import auditable_event
from ..database import db
from ..extensions import oauth
from ..models.fhir import bundle_results
from ..models.identifier import (
    Identifier,
    UserIdentifier,
    parse_identifier_params,
)
from ..models.overall_status import OverallStatus
from ..models.questionnaire_bank import QuestionnaireBank
from ..models.questionnaire_response import QuestionnaireResponse
from ..models.qb_timeline import QBT, update_users_QBT
from ..models.reference import Reference
from ..models.role import ROLE
from ..models.user import User, current_user, get_user_or_abort
from .crossdomain import crossdomain
from .demographics import demographics

patient_api = Blueprint('patient_api', __name__)

@patient_api.route('/api/patient/')
@crossdomain()
@oauth.require_oauth()
def patient_search():
    """Looks up patient(s) from given parameters, returns FHIR Patient bundle

    Takes key=value pairs to look up.  Email, identifier searches supported.

    Example searches:
        /api/patient?email=username@server.com
        /api/patient?identifier=http://us.truenth.org/identity-codes/external-study-id|123-45-678

    Identifier search pattern:
        ?identifier=<system>|<value>

    Deleted users and users for which the authenticated user does not have
    permission to view will NOT be included in the results.

    NB - the results are restricted to users with the patient role.  It is
    therefore possible to get no results from this and still see a unique email
    collision from existing non-patient users.

    NB - currently out of FHIR DSTU2 spec by default.  Include query string
    parameter ``patch_dstu2=True`` to properly return a FHIR bundle resource
    (https://www.hl7.org/fhir/DSTU2/bundle.html) naming the ``total`` matches
    and references to all matching patients.  With ``patch_dstu2=True``, the
    total will be zero if no matches are found, whereas the default (old,
    non-compliant) behavior is to return a 404 when no match is found.
    Please consider using the ``patch_dstu2=True`` parameter, as this will
    become the default behavior in the future.

    Returns a FHIR bundle resource (https://www.hl7.org/fhir/DSTU2/bundle.html)
    formatted in JSON for all matching valid, accessible patients, with
    ``patch_dstu2=True`` set (preferred).  Default returns single patient on a
    match, 404 on no match, and 400 for multiple as it's not supported.

    ---
    tags:
      - Patient
    operationId: patient_search
    produces:
      - application/json
    parameters:
      - name: search_parameters
        in: query
        description:
            Search parameters, such as `email` or `identifier`.  For
            identifier, URL-encode the `system` and `value` using '|' (pipe)
            delimiter, i.e. `api/patient/?identifier=http://fake.org/id|12a7`
        required: true
        type: string
      - name: patch_dstu2
        in: query
        description: whether or not to return DSTU2 compliant bundle
        required: false
        type: boolean
        default: false
    responses:
      200:
        description:
          Returns FHIR patient resource
          (https://www.hl7.org/fhir/DSTU2/patient.html) in JSON if a match
          is found.  Otherwise responds with a 404 status code.
      401:
        description:
          if missing valid OAuth token
      404:
        description:
          if there is no match found, or the user lacks permission to look
          up details on the match.
    security:
      - ServiceToken: []

    """
    if not request.args.items():
        abort(400, "missing search criteria")

    query = User.query.filter(User.deleted_id.is_(None))
    for k, v in request.args.items():
        if k == 'email':
            query = query.filter(User.email == v)
        elif k == 'identifier':
            system, value = parse_identifier_params(v)
            if not (system and value):
                abort(400, "need 'system' and 'value' to look up identifier")

            query = query.join(UserIdentifier).join(
                Identifier).filter(and_(
                    UserIdentifier.identifier_id == Identifier.id,
                    Identifier.system == system,
                    Identifier._value == value))
        elif k == 'patch_dstu2':
            # not search criteria, but valid
            continue
        else:
            abort(400, "can't search on '{}' at this time".format(k))

    # Validate permissions to see every requested user - omitting those w/o
    patients = []
    for user in query:
        try:
            current_user().check_role(permission='view', other_id=user.id)
            if user.has_role(ROLE.PATIENT.value):
                patients.append(
                    {'resource': Reference.patient(user.id).as_fhir()})
        except Unauthorized:
            # Mask unauthorized as a not-found.  Don't want unauthed users
            # farming information - i.e. don't add to results
            auditable_event("looking up users with inadequate permission",
                            user_id=current_user().id, subject_id=user.id,
                            context='authentication')

    if request.args.get('patch_dstu2', False):
        link = {"rel": "self", "href": request.url}
        return jsonify(bundle_results(elements=patients, links=[link]))
    else:
        # Emulate old results
        if len(patients) == 0:
            abort(404)
        if len(patients) > 1:
            abort(400, "multiple results found, include `patch_dstu2=True`")

        ref = Reference.parse(patients[0]['resource'])
        return demographics(patient_id=ref.id)

@patient_api.route('/api/patient/<int:patient_id>/deceased', methods=('POST',))
@crossdomain()
@oauth.require_oauth()
def post_patient_deceased(patient_id):
    """POST deceased datetime or status for a patient

    This convenience API wraps the ability to set a patient's
    deceased status and deceased date time - generally the /api/demographics
    API should be preferred.

    ---
    operationId: deceased
    tags:
      - Patient
    produces:
      - application/json
    parameters:
      - name: patient_id
        in: path
        description: TrueNTH user ID
        required: true
        type: integer
        format: int64
      - in: body
        name: body
        schema:
          id: deceased_details
          properties:
            deceasedBoolean:
              type: string
              description:
                true or false.  Implicitly true with deceasedDateTime value
            deceasedDateTime:
              type: string
              description: valid FHIR datetime string defining time of death
    responses:
      200:
        description:
          Returns updated [FHIR patient
          resource](http://www.hl7.org/fhir/patient.html) in JSON.
      400:
        description:
          if given parameters don't function, such as a false
          deceasedBoolean AND a deceasedDateTime value.
      401:
        description:
          if missing valid OAuth token or logged-in user lacks permission
          to view requested patient
    security:
      - ServiceToken: []

    """
    current_user().check_role(permission='edit', other_id=patient_id)
    patient = get_user_or_abort(patient_id)
    if not request.json or set(request.json.keys()).isdisjoint(
            {'deceasedDateTime', 'deceasedBoolean'}):
        abort(400, "Requires deceasedDateTime or deceasedBoolean in JSON")

    patient.update_deceased(request.json)
    db.session.commit()
    auditable_event("updated demographics on user {0} from input {1}".format(
        patient.id, json.dumps(request.json)), user_id=current_user().id,
        subject_id=patient.id, context='user')

    return jsonify(patient.as_fhir(include_empties=False))

@patient_api.route(
    '/api/patient/<int:patient_id>/birthdate', methods=('POST',))
@patient_api.route(
    '/api/patient/<int:patient_id>/birthDate', methods=('POST',))
@oauth.require_oauth()
def post_patient_dob(patient_id):
    """POST date of birth for a patient

    This convenience API wraps the ability to set a patient's birthDate.
    Generally the /api/demographics API should be preferred.

    ---
    tags:
      - Patient
    produces:
      - application/json
    parameters:
      - name: patient_id
        in: path
        description: TrueNTH user ID
        required: true
        type: integer
        format: int64
      - in: body
        name: body
        schema:
          id: dob_details
          properties:
            birthDate:
              type: string
              description: valid FHIR date string defining date of birth
    responses:
      200:
        description:
          Returns updated [FHIR patient
          resource](http://www.hl7.org/fhir/patient.html) in JSON.
      400:
        description:
          if given parameters don't validate
      401:
        description:
          if missing valid OAuth token or logged-in user lacks permission
          to edit requested patient
    security:
      - ServiceToken: []

    """
    current_user().check_role(permission='edit', other_id=patient_id)
    patient = get_user_or_abort(patient_id)
    if not request.json or 'birthDate' not in request.json:
        abort(400, "Requires `birthDate` in JSON")

    patient.update_birthdate(request.json)
    db.session.commit()
    auditable_event("updated demographics on user {0} from input {1}".format(
        patient.id, json.dumps(request.json)), user_id=current_user().id,
        subject_id=patient.id, context='user')

    return jsonify(patient.as_fhir(include_empties=False))

@patient_api.route('/api/patient/<int:patient_id>/timeline')
@oauth.require_oauth()
def patient_timeline(patient_id):
    from ..date_tools import FHIR_datetime
    from ..models.qbd import QBD
    from ..models.qb_status import QB_Status
    from ..models.questionnaire_bank import visit_name
    from ..trace import dump_trace, establish_trace

    trace = request.args.get('trace', False)
    if trace:
        establish_trace("BEGIN time line lookup for {}".format(patient_id))

    current_user().check_role(permission='view', other_id=patient_id)

    purge = request.args.get('purge', False)
    try:
        update_users_QBT(patient_id, invalidate_existing=purge)
    except ValueError as ve:
        abort(500, ve.message)

    results = []
    # We order by at (to get the latest status for a given QB) and
    # secondly by id, as on rare occasions, the time (`at`) of
    #  `due` == `completed`, but the row insertion defines priority
    for qbt in QBT.query.filter(QBT.user_id == patient_id).order_by(
            QBT.at, QBT.id):
        # build qbd for visit name
        qbd = QBD(
            relative_start=qbt.at, qb_id=qbt.qb_id,
            iteration=qbt.qb_iteration, recur_id=qbt.qb_recur_id)
        if qbt.status == OverallStatus.withdrawn:
            results.append({
                'status': str(qbt.status),
                'at': FHIR_datetime.as_fhir(qbt.at)})
        else:
            results.append({
                'status': str(qbt.status),
                'at': FHIR_datetime.as_fhir(qbt.at),
                'qb (id, iteration)': "{} ({}, {})".format(
                    qbd.questionnaire_bank.name, qbd.qb_id, qbd.iteration),
                'visit': visit_name(qbd)})

    qb_names = {qb.id: qb.name for qb in QuestionnaireBank.query.all()}

    qnrs = QuestionnaireResponse.query.filter(
        QuestionnaireResponse.subject_id == patient_id).order_by(
        QuestionnaireResponse.authored)
    posted = [{
        'at, qb, iteration, status, name': "{}, {}, {}, {}, {}".format(
            qnr.authored,
            qb_names.get(qnr.questionnaire_bank_id),
            qnr.qb_iteration,
            qnr.status,
            qnr.document['questionnaire']['reference'].split('/')[-1])
        } for qnr in qnrs]

    qbstatus = QB_Status(
        user=User.query.get(patient_id), as_of_date=datetime.utcnow())
    prev_qbd = qbstatus.prev_qbd
    current = qbstatus.current_qbd()
    next_qbd = qbstatus.next_qbd
    status = {
        'overall': str(qbstatus.overall_status),
        'previous QBD': prev_qbd.as_json() if prev_qbd else None,
        'current QBD': current.as_json() if current else None,
        'next QBD': next_qbd.as_json() if next_qbd else None,
    }
    if current:
        status['due'] = qbstatus.due_date
        status['overdue'] = qbstatus.overdue_date
        status['expired'] = qbstatus.expired_date
        status['needing-full'] = qbstatus.instruments_needing_full_assessment()
        status['in-progress'] = qbstatus.instruments_in_progress()
        status['completed'] = qbstatus.instruments_completed()

    if trace:
        return jsonify(
            status=status,
            posted=posted,
            timeline=results,
            trace=dump_trace("END time line lookup"))
    return jsonify(status=status, posted=posted, timeline=results)
<|endoftext|>


<commit_before>"""Patient API - implements patient specific views such as patient search

NB - this is not to be confused with 'patients', which defines views
for staff

"""
from flask import abort, Blueprint, jsonify, request
import json
from sqlalchemy import and_
from werkzeug.exceptions import Unauthorized

from ..audit import auditable_event
from .demographics import demographics
from ..extensions import oauth
from ..models.identifier import Identifier, UserIdentifier
from ..models.role import ROLE
from ..models.user import current_user, get_user_or_abort, User

patient_api = Blueprint('patient_api', __name__)

@patient_api.route('/api/patient/')
@oauth.require_oauth()
def patient_search():
    """Looks up patient from given parameters, returns FHIR Patient if found

    Takes key=value pairs to look up.  At this time, only email is supported.

    Example search:
        /api/patient?email=username@server.com

    Returns a FHIR patient resource (http://www.hl7.org/fhir/patient.html)
    formatted in JSON if a match is found, 404 otherwise.

    NB - the results are restricted to users with the patient role.  It is
    therefore possible to get no results from this and still see a unique email
    collision from existing non-patient users.

    ---
    tags:
      - Patient
    operationId: patient_search
    produces:
      - application/json
    parameters:
      - name: search_parameters
        in: query
        description:
            Search parameters, such as `email` or `identifier`.  For
            identifier, URL-encode a serialized JSON object with the `system`
            and `value` attributes defined.  An example looking up a patient
            by a fake identifier
            `api/patient/?identifier={"system":%20"http://fake.org/id",%20"value":%20"12a7"}`
        required: true
        type: string
    responses:
      200:
        description:
          Returns FHIR patient resource (http://www.hl7.org/fhir/patient.html)
          in JSON if a match is found.  Otherwise responds with a 404 status
          code.
      401:
        description:
          if missing valid OAuth token
      404:
        description:
          if there is no match found, or the user lacks permission to look
          up details on the match.

    """
    search_params = {}
    for k,v in request.args.items():
        if k == 'email':
            search_params[k] = v
        elif k == 'identifier':
            try:
                ident_dict = json.loads(v)
                if not (ident_dict.get('system') and ident_dict.get('value')):
                    abort(400,
                          "need 'system' and 'value' to look up identifier")
                ui = UserIdentifier.query.join(
                    Identifier).filter(and_(
                        UserIdentifier.identifier_id==Identifier.id,
                        Identifier.system==ident_dict['system'],
                        Identifier._value==ident_dict['value'])).first()
                if ui:
                    search_params['id'] = ui.user_id
            except ValueError:
                abort(400, "Ill formed identifier parameter")
        else:
            abort(400, "can't search on '{}' at this time".format(k))

    if not search_params:
        # Nothing found worth looking up above
        abort(404)
    match = User.query.filter_by(**search_params)
    if match.count() > 1:
        abort(400, "can't yet bundle results, multiple found")
    if match.count() == 1:
        user = match.one()
        try:
            current_user().check_role(permission='view', other_id=user.id)
            if user.has_role(ROLE.PATIENT):
                return demographics(patient_id=user.id)
        except Unauthorized:
            # Mask unauthorized as a not-found.  Don't want unauthed users
            # farming information
            auditable_event("looking up users with inadequate permission",
                            user_id=current_user().id, subject_id=user.id,
                            context='authentication')
            abort(404)
    abort(404)

@patient_api.route('/api/patient/<int:patient_id>/deceased', methods=('POST',))
@oauth.require_oauth()
def post_patient_deceased(patient_id):
    """POST deceased datetime or status for a patient

    This convenience API wraps the ability to set a patient's
    deceased status and deceased date time - generally the /api/demographics
    API should be preferred.

    ---
    operationId: deceased
    tags:
      - Patient
    produces:
      - application/json
    parameters:
      - name: patient_id
        in: path
        description: TrueNTH user ID
        required: true
        type: integer
        format: int64
      - in: body
        name: body
        schema:
          id: deceased_details
          properties:
            deceasedBoolean:
              type: string
              description:
                true or false.  Implicitly true with deceasedDateTime value
            deceasedDateTime:
              type: string
              description: valid FHIR datetime string defining time of death
    responses:
      200:
        description:
          Returns updated [FHIR patient
          resource](http://www.hl7.org/fhir/patient.html) in JSON.
      400:
        description:
          if given parameters don't function, such as a false deceasedBoolean AND
          a deceasedDateTime value.
      401:
        description:
          if missing valid OAuth token or logged-in user lacks permission
          to view requested patient
      409:
        description:
          if attempting to POST new deceased data for a patient whom already
          has a defined deceased value.

    """
    current_user().check_role(permission='edit', other_id=patient_id)
    patient = get_user_or_abort(patient_id)
    if not request.json and set(request.json.keys()).isdisjoint(
            {'deceasedDateTime', 'deceasedBoolean'}):
        abort(400, "Requires deceasedDateTime or deceasedBoolean in JSON")

    if patient.deceased and (
            request.json.get('deceasedBoolean') or
            request.json.get('deceasedDateTime')):
        abort(409, "Deceased value already set for patient {}".format(
            patient_id))

    patient.update_deceased(request.json)
    return jsonify(patient.as_fhir(include_empties=False))

@patient_api.route('/api/patient/<int:patient_id>/birthdate', methods=('POST',))
@patient_api.route('/api/patient/<int:patient_id>/birthDate', methods=('POST',))
@oauth.require_oauth()
def post_patient_dob(patient_id):
    """POST date of birth for a patient

    This convenience API wraps the ability to set a patient's birthDate - generally
    the /api/demographics API should be preferred.

    ---
    operationId: dob
    tags:
      - Patient
    produces:
      - application/json
    parameters:
      - name: patient_id
        in: path
        description: TrueNTH user ID
        required: true
        type: integer
        format: int64
      - in: body
        name: body
        schema:
          id: dob_details
          properties:
            birthDate:
              type: string
              description: valid FHIR date string defining date of birth
    responses:
      200:
        description:
          Returns updated [FHIR patient
          resource](http://www.hl7.org/fhir/patient.html) in JSON.
      400:
        description:
          if given parameters don't validate
      401:
        description:
          if missing valid OAuth token or logged-in user lacks permission
          to edit requested patient
      409:
        description:
          if attempting to POST new birthDate data for a patient whom already
          has a defined birthDate value.

    """
    current_user().check_role(permission='edit', other_id=patient_id)
    patient = get_user_or_abort(patient_id)
    if not request.json and 'birthDate' not in request.json:
        abort(400, "Requires `birthDate` in JSON")

    if patient.birthdate:
        abort(409, "birthDate value already set for patient {}".format(
            patient_id))

    patient.update_birthdate(request.json)
    return jsonify(patient.as_fhir(include_empties=False))
<commit_msg>Corrected two demorgon's logic errors - if either POST failed to send JSON, it would have caused a server error rather than raising the desired 400.<commit_after>"""Patient API - implements patient specific views such as patient search

NB - this is not to be confused with 'patients', which defines views
for staff

"""
from flask import abort, Blueprint, jsonify, request
import json
from sqlalchemy import and_
from werkzeug.exceptions import Unauthorized

from ..audit import auditable_event
from .demographics import demographics
from ..extensions import oauth
from ..models.identifier import Identifier, UserIdentifier
from ..models.role import ROLE
from ..models.user import current_user, get_user_or_abort, User

patient_api = Blueprint('patient_api', __name__)

@patient_api.route('/api/patient/')
@oauth.require_oauth()
def patient_search():
    """Looks up patient from given parameters, returns FHIR Patient if found

    Takes key=value pairs to look up.  At this time, only email is supported.

    Example search:
        /api/patient?email=username@server.com

    Returns a FHIR patient resource (http://www.hl7.org/fhir/patient.html)
    formatted in JSON if a match is found, 404 otherwise.

    NB - the results are restricted to users with the patient role.  It is
    therefore possible to get no results from this and still see a unique email
    collision from existing non-patient users.

    ---
    tags:
      - Patient
    operationId: patient_search
    produces:
      - application/json
    parameters:
      - name: search_parameters
        in: query
        description:
            Search parameters, such as `email` or `identifier`.  For
            identifier, URL-encode a serialized JSON object with the `system`
            and `value` attributes defined.  An example looking up a patient
            by a fake identifier
            `api/patient/?identifier={"system":%20"http://fake.org/id",%20"value":%20"12a7"}`
        required: true
        type: string
    responses:
      200:
        description:
          Returns FHIR patient resource (http://www.hl7.org/fhir/patient.html)
          in JSON if a match is found.  Otherwise responds with a 404 status
          code.
      401:
        description:
          if missing valid OAuth token
      404:
        description:
          if there is no match found, or the user lacks permission to look
          up details on the match.

    """
    search_params = {}
    for k,v in request.args.items():
        if k == 'email':
            search_params[k] = v
        elif k == 'identifier':
            try:
                ident_dict = json.loads(v)
                if not (ident_dict.get('system') and ident_dict.get('value')):
                    abort(400,
                          "need 'system' and 'value' to look up identifier")
                ui = UserIdentifier.query.join(
                    Identifier).filter(and_(
                        UserIdentifier.identifier_id==Identifier.id,
                        Identifier.system==ident_dict['system'],
                        Identifier._value==ident_dict['value'])).first()
                if ui:
                    search_params['id'] = ui.user_id
            except ValueError:
                abort(400, "Ill formed identifier parameter")
        else:
            abort(400, "can't search on '{}' at this time".format(k))

    if not search_params:
        # Nothing found worth looking up above
        abort(404)
    match = User.query.filter_by(**search_params)
    if match.count() > 1:
        abort(400, "can't yet bundle results, multiple found")
    if match.count() == 1:
        user = match.one()
        try:
            current_user().check_role(permission='view', other_id=user.id)
            if user.has_role(ROLE.PATIENT):
                return demographics(patient_id=user.id)
        except Unauthorized:
            # Mask unauthorized as a not-found.  Don't want unauthed users
            # farming information
            auditable_event("looking up users with inadequate permission",
                            user_id=current_user().id, subject_id=user.id,
                            context='authentication')
            abort(404)
    abort(404)

@patient_api.route('/api/patient/<int:patient_id>/deceased', methods=('POST',))
@oauth.require_oauth()
def post_patient_deceased(patient_id):
    """POST deceased datetime or status for a patient

    This convenience API wraps the ability to set a patient's
    deceased status and deceased date time - generally the /api/demographics
    API should be preferred.

    ---
    operationId: deceased
    tags:
      - Patient
    produces:
      - application/json
    parameters:
      - name: patient_id
        in: path
        description: TrueNTH user ID
        required: true
        type: integer
        format: int64
      - in: body
        name: body
        schema:
          id: deceased_details
          properties:
            deceasedBoolean:
              type: string
              description:
                true or false.  Implicitly true with deceasedDateTime value
            deceasedDateTime:
              type: string
              description: valid FHIR datetime string defining time of death
    responses:
      200:
        description:
          Returns updated [FHIR patient
          resource](http://www.hl7.org/fhir/patient.html) in JSON.
      400:
        description:
          if given parameters don't function, such as a false deceasedBoolean AND
          a deceasedDateTime value.
      401:
        description:
          if missing valid OAuth token or logged-in user lacks permission
          to view requested patient
      409:
        description:
          if attempting to POST new deceased data for a patient whom already
          has a defined deceased value.

    """
    current_user().check_role(permission='edit', other_id=patient_id)
    patient = get_user_or_abort(patient_id)
    if not request.json or set(request.json.keys()).isdisjoint(
            {'deceasedDateTime', 'deceasedBoolean'}):
        abort(400, "Requires deceasedDateTime or deceasedBoolean in JSON")

    if patient.deceased and (
            request.json.get('deceasedBoolean') or
            request.json.get('deceasedDateTime')):
        abort(409, "Deceased value already set for patient {}".format(
            patient_id))

    patient.update_deceased(request.json)
    return jsonify(patient.as_fhir(include_empties=False))

@patient_api.route('/api/patient/<int:patient_id>/birthdate', methods=('POST',))
@patient_api.route('/api/patient/<int:patient_id>/birthDate', methods=('POST',))
@oauth.require_oauth()
def post_patient_dob(patient_id):
    """POST date of birth for a patient

    This convenience API wraps the ability to set a patient's birthDate - generally
    the /api/demographics API should be preferred.

    ---
    operationId: dob
    tags:
      - Patient
    produces:
      - application/json
    parameters:
      - name: patient_id
        in: path
        description: TrueNTH user ID
        required: true
        type: integer
        format: int64
      - in: body
        name: body
        schema:
          id: dob_details
          properties:
            birthDate:
              type: string
              description: valid FHIR date string defining date of birth
    responses:
      200:
        description:
          Returns updated [FHIR patient
          resource](http://www.hl7.org/fhir/patient.html) in JSON.
      400:
        description:
          if given parameters don't validate
      401:
        description:
          if missing valid OAuth token or logged-in user lacks permission
          to edit requested patient
      409:
        description:
          if attempting to POST new birthDate data for a patient whom already
          has a defined birthDate value.

    """
    current_user().check_role(permission='edit', other_id=patient_id)
    patient = get_user_or_abort(patient_id)
    if not request.json or 'birthDate' not in request.json:
        abort(400, "Requires `birthDate` in JSON")

    if patient.birthdate:
        abort(409, "birthDate value already set for patient {}".format(
            patient_id))

    patient.update_birthdate(request.json)
    return jsonify(patient.as_fhir(include_empties=False))
<|endoftext|>


<commit_before>"""distutils.unixccompiler

Contains the UnixCCompiler class, a subclass of CCompiler that handles
the "typical" Unix-style command-line C compiler:
  * macros defined with -Dname[=value]
  * macros undefined with -Uname
  * include search directories specified with -Idir
  * libraries specified with -lllib
  * library search directories specified with -Ldir
  * compile handled by 'cc' (or similar) executable with -c option:
    compiles .c to .o
  * link static library handled by 'ar' command (possibly with 'ranlib')
  * link shared library handled by 'cc -shared'
"""

import os
import sys
import re
import shlex

from distutils import sysconfig
from distutils.dep_util import newer
from distutils.ccompiler import CCompiler, gen_preprocess_options, gen_lib_options
from distutils.errors import DistutilsExecError, CompileError, LibError, LinkError
from distutils import log
from ._macos_compat import compiler_fixup

# XXX Things not currently handled:
#   * optimization/debug/warning flags; we just use whatever's in Python's
#     Makefile and live with it.  Is this adequate?  If not, we might
#     have to have a bunch of subclasses GNUCCompiler, SGICCompiler,
#     SunCCompiler, and I suspect down that road lies madness.
#   * even if we don't know a warning flag from an optimization flag,
#     we need some way for outsiders to feed preprocessor/compiler/linker
#     flags in to us -- eg. a sysadmin might want to mandate certain flags
#     via a site config file, or a user might want to set something for
#     compiling this module distribution only via the setup.py command
#     line, whatever.  As long as these options come from something on the
#     current system, they can be as system-dependent as they like, and we
#     should just happily stuff them into the preprocessor/compiler/linker
#     options and carry on.

def _split_env(cmd):
    """
    For macOS, split command into 'env' portion (if any)
    and the rest of the linker command.

    >>> _split_env(['a', 'b', 'c'])
    ([], ['a', 'b', 'c'])
    >>> _split_env(['/usr/bin/env', 'A=3', 'gcc'])
    (['/usr/bin/env', 'A=3'], ['gcc'])
    """
    pivot = 0
    if os.path.basename(cmd[0]) == "env":
        pivot = 1
        while '=' in cmd[pivot]:
            pivot += 1
    return cmd[:pivot], cmd[pivot:]

def _split_aix(cmd):
    """
    AIX platforms prefix the compiler with the ld_so_aix
    script, so split that from the linker command.

    >>> _split_aix(['a', 'b', 'c'])
    ([], ['a', 'b', 'c'])
    >>> _split_aix(['/bin/foo/ld_so_aix', 'gcc'])
    (['/bin/foo/ld_so_aix'], ['gcc'])
    """
    pivot = os.path.basename(cmd[0]) == 'ld_so_aix'
    return cmd[:pivot], cmd[pivot:]

def _linker_params(linker_cmd, compiler_cmd):
    """
    The linker command usually begins with the compiler
    command (possibly multiple elements), followed by zero or more
    params for shared library building.

    If the LDSHARED env variable overrides the linker command,
    however, the commands may not match.

    Return the best guess of the linker parameters by stripping
    the linker command. If the compiler command does not
    match the linker command, assume the linker command is
    just the first element.

    >>> _linker_params('gcc foo bar'.split(), ['gcc'])
    ['foo', 'bar']
    >>> _linker_params('gcc foo bar'.split(), ['other'])
    ['foo', 'bar']
    >>> _linker_params('ccache gcc foo bar'.split(), 'ccache gcc'.split())
    ['foo', 'bar']
    >>> _linker_params(['gcc'], ['gcc'])
    []
    """
    c_len = len(compiler_cmd)
    pivot = c_len if linker_cmd[:c_len] == compiler_cmd else 1
    return linker_cmd[pivot:]

class UnixCCompiler(CCompiler):

    compiler_type = 'unix'

    # These are used by CCompiler in two places: the constructor sets
    # instance attributes 'preprocessor', 'compiler', etc. from them, and
    # 'set_executable()' allows any of these to be set.  The defaults here
    # are pretty generic; they will probably have to be set by an outsider
    # (eg. using information discovered by the sysconfig about building
    # Python extensions).
    executables = {
        'preprocessor': None,
        'compiler': ["cc"],
        'compiler_so': ["cc"],
        'compiler_cxx': ["cc"],
        'linker_so': ["cc", "-shared"],
        'linker_exe': ["cc"],
        'archiver': ["ar", "-cr"],
        'ranlib': None,
    }

    if sys.platform[:6] == "darwin":
        executables['ranlib'] = ["ranlib"]

    # Needed for the filename generation methods provided by the base
    # class, CCompiler.  NB. whoever instantiates/uses a particular
    # UnixCCompiler instance should set 'shared_lib_ext' -- we set a
    # reasonable common default here, but it's not necessarily used on all
    # Unices!

    src_extensions = [".c", ".C", ".cc", ".cxx", ".cpp", ".m"]
    obj_extension = ".o"
    static_lib_extension = ".a"
    shared_lib_extension = ".so"
    dylib_lib_extension = ".dylib"
    xcode_stub_lib_extension = ".tbd"
    static_lib_format = shared_lib_format = dylib_lib_format = "lib%s%s"
    xcode_stub_lib_format = dylib_lib_format
    if sys.platform == "cygwin":
        exe_extension = ".exe"

    def preprocess(
        self,
        source,
        output_file=None,
        macros=None,
        include_dirs=None,
        extra_preargs=None,
        extra_postargs=None,
    ):
        fixed_args = self._fix_compile_args(None, macros, include_dirs)
        ignore, macros, include_dirs = fixed_args
        pp_opts = gen_preprocess_options(macros, include_dirs)
        pp_args = self.preprocessor + pp_opts
        if output_file:
            pp_args.extend(['-o', output_file])
        if extra_preargs:
            pp_args[:0] = extra_preargs
        if extra_postargs:
            pp_args.extend(extra_postargs)
        pp_args.append(source)

        # reasons to preprocess:
        # - force is indicated
        # - output is directed to stdout
        # - source file is newer than the target
        preprocess = self.force or output_file is None or newer(source, output_file)
        if not preprocess:
            return

        if output_file:
            self.mkpath(os.path.dirname(output_file))

        try:
            self.spawn(pp_args)
        except DistutilsExecError as msg:
            raise CompileError(msg)

    def _compile(self, obj, src, ext, cc_args, extra_postargs, pp_opts):
        compiler_so = compiler_fixup(self.compiler_so, cc_args + extra_postargs)
        try:
            self.spawn(compiler_so + cc_args + [src, '-o', obj] + extra_postargs)
        except DistutilsExecError as msg:
            raise CompileError(msg)

    def create_static_lib(
        self, objects, output_libname, output_dir=None, debug=0, target_lang=None
    ):
        objects, output_dir = self._fix_object_args(objects, output_dir)

        output_filename = self.library_filename(output_libname, output_dir=output_dir)

        if self._need_link(objects, output_filename):
            self.mkpath(os.path.dirname(output_filename))
            self.spawn(self.archiver + [output_filename] + objects + self.objects)

            # Not many Unices required ranlib anymore -- SunOS 4.x is, I
            # think the only major Unix that does.  Maybe we need some
            # platform intelligence here to skip ranlib if it's not
            # needed -- or maybe Python's configure script took care of
            # it for us, hence the check for leading colon.
            if self.ranlib:
                try:
                    self.spawn(self.ranlib + [output_filename])
                except DistutilsExecError as msg:
                    raise LibError(msg)
        else:
            log.debug("skipping %s (up-to-date)", output_filename)

    def link(
        self,
        target_desc,
        objects,
        output_filename,
        output_dir=None,
        libraries=None,
        library_dirs=None,
        runtime_library_dirs=None,
        export_symbols=None,
        debug=0,
        extra_preargs=None,
        extra_postargs=None,
        build_temp=None,
        target_lang=None,
    ):
        objects, output_dir = self._fix_object_args(objects, output_dir)
        fixed_args = self._fix_lib_args(libraries, library_dirs, runtime_library_dirs)
        libraries, library_dirs, runtime_library_dirs = fixed_args

        lib_opts = gen_lib_options(self, library_dirs, runtime_library_dirs, libraries)
        if not isinstance(output_dir, (str, type(None))):
            raise TypeError("'output_dir' must be a string or None")
        if output_dir is not None:
            output_filename = os.path.join(output_dir, output_filename)

        if self._need_link(objects, output_filename):
            ld_args = objects + self.objects + lib_opts + ['-o', output_filename]
            if debug:
                ld_args[:0] = ['-g']
            if extra_preargs:
                ld_args[:0] = extra_preargs
            if extra_postargs:
                ld_args.extend(extra_postargs)
            self.mkpath(os.path.dirname(output_filename))
            try:
                # Select a linker based on context: linker_exe when
                # building an executable or linker_so (with shared options)
                # when building a shared library.
                building_exe = target_desc == CCompiler.EXECUTABLE
                linker = (self.linker_exe if building_exe else self.linker_so)[:]

                if target_lang == "c++" and self.compiler_cxx:
                    env, linker_ne = _split_env(linker)
                    aix, linker_na = _split_aix(linker_ne)
                    _, compiler_cxx_ne = _split_env(self.compiler_cxx)
                    _, linker_exe_ne = _split_env(self.linker_exe)

                    params = _linker_params(linker_na, linker_exe_ne)
                    linker = env + aix + compiler_cxx_ne + params

                linker = compiler_fixup(linker, ld_args)

                self.spawn(linker + ld_args)
            except DistutilsExecError as msg:
                raise LinkError(msg)
        else:
            log.debug("skipping %s (up-to-date)", output_filename)

    # -- Miscellaneous methods -----------------------------------------
    # These are all used by the 'gen_lib_options() function, in
    # ccompiler.py.

    def library_dir_option(self, dir):
        return "-L" + dir

    def _is_gcc(self):
        cc_var = sysconfig.get_config_var("CC")
        compiler = os.path.basename(shlex.split(cc_var)[0])
        return "gcc" in compiler or "g++" in compiler

    def runtime_library_dir_option(self, dir):
        # XXX Hackish, at the very least.  See Python bug #445902:
        # http://sourceforge.net/tracker/index.php
        #   ?func=detail&aid=445902&group_id=5470&atid=105470
        # Linkers on different platforms need different options to
        # specify that directories need to be added to the list of
        # directories searched for dependencies when a dynamic library
        # is sought.  GCC on GNU systems (Linux, FreeBSD, ...) has to
        # be told to pass the -R option through to the linker, whereas
        # other compilers and gcc on other systems just know this.
        # Other compilers may need something slightly different.  At
        # this time, there's no way to determine this information from
        # the configuration data stored in the Python installation, so
        # we use this hack.
        if sys.platform[:6] == "darwin":
            from distutils.util import get_macosx_target_ver, split_version

            macosx_target_ver = get_macosx_target_ver()
            if macosx_target_ver and split_version(macosx_target_ver) >= [10, 5]:
                return "-Wl,-rpath," + dir
            else:  # no support for -rpath on earlier macOS versions
                return "-L" + dir
        elif sys.platform[:7] == "freebsd":
            return "-Wl,-rpath=" + dir
        elif sys.platform[:5] == "hp-ux":
            return [
                "-Wl,+s" if self._is_gcc() else "+s",
                "-L" + dir,
            ]

        # For all compilers, `-Wl` is the presumed way to
        # pass a compiler option to the linker and `-R` is
        # the way to pass an RPATH.
        if sysconfig.get_config_var("GNULD") == "yes":
            # GNU ld needs an extra option to get a RUNPATH
            # instead of just an RPATH.
            return "-Wl,--enable-new-dtags,-R" + dir
        else:
            return "-Wl,-R" + dir

    def library_option(self, lib):
        return "-l" + lib

    def find_library_file(self, dirs, lib, debug=0):
        shared_f = self.library_filename(lib, lib_type='shared')
        dylib_f = self.library_filename(lib, lib_type='dylib')
        xcode_stub_f = self.library_filename(lib, lib_type='xcode_stub')
        static_f = self.library_filename(lib, lib_type='static')

        if sys.platform == 'darwin':
            # On OSX users can specify an alternate SDK using
            # '-isysroot', calculate the SDK root if it is specified
            # (and use it further on)
            #
            # Note that, as of Xcode 7, Apple SDKs may contain textual stub
            # libraries with .tbd extensions rather than the normal .dylib
            # shared libraries installed in /.  The Apple compiler tool
            # chain handles this transparently but it can cause problems
            # for programs that are being built with an SDK and searching
            # for specific libraries.  Callers of find_library_file need to
            # keep in mind that the base filename of the returned SDK library
            # file might have a different extension from that of the library
            # file installed on the running system, for example:
            #   /Applications/Xcode.app/Contents/Developer/Platforms/
            #       MacOSX.platform/Developer/SDKs/MacOSX10.11.sdk/
            #       usr/lib/libedit.tbd
            # vs
            #   /usr/lib/libedit.dylib
            cflags = sysconfig.get_config_var('CFLAGS')
            m = re.search(r'-isysroot\s*(\S+)', cflags)
            if m is None:
                sysroot = '/'
            else:
                sysroot = m.group(1)

        for dir in dirs:
            shared = os.path.join(dir, shared_f)
            dylib = os.path.join(dir, dylib_f)
            static = os.path.join(dir, static_f)
            xcode_stub = os.path.join(dir, xcode_stub_f)

            if sys.platform == 'darwin' and (
                dir.startswith('/System/')
                or (dir.startswith('/usr/') and not dir.startswith('/usr/local/'))
            ):

                shared = os.path.join(sysroot, dir[1:], shared_f)
                dylib = os.path.join(sysroot, dir[1:], dylib_f)
                static = os.path.join(sysroot, dir[1:], static_f)
                xcode_stub = os.path.join(sysroot, dir[1:], xcode_stub_f)

            # We're second-guessing the linker here, with not much hard
            # data to go on: GCC seems to prefer the shared library, so I'm
            # assuming that *all* Unix C compilers do.  And of course I'm
            # ignoring even GCC's "-static" option.  So sue me.
            if os.path.exists(dylib):
                return dylib
            elif os.path.exists(xcode_stub):
                return xcode_stub
            elif os.path.exists(shared):
                return shared
            elif os.path.exists(static):
                return static

        # Oops, didn't find it in *any* of 'dirs'
        return None
<commit_msg>Extract _library_root to disentangle that concern from the other calculations.<commit_after>"""distutils.unixccompiler

Contains the UnixCCompiler class, a subclass of CCompiler that handles
the "typical" Unix-style command-line C compiler:
  * macros defined with -Dname[=value]
  * macros undefined with -Uname
  * include search directories specified with -Idir
  * libraries specified with -lllib
  * library search directories specified with -Ldir
  * compile handled by 'cc' (or similar) executable with -c option:
    compiles .c to .o
  * link static library handled by 'ar' command (possibly with 'ranlib')
  * link shared library handled by 'cc -shared'
"""

import os
import sys
import re
import shlex

from distutils import sysconfig
from distutils.dep_util import newer
from distutils.ccompiler import CCompiler, gen_preprocess_options, gen_lib_options
from distutils.errors import DistutilsExecError, CompileError, LibError, LinkError
from distutils import log
from ._macos_compat import compiler_fixup

# XXX Things not currently handled:
#   * optimization/debug/warning flags; we just use whatever's in Python's
#     Makefile and live with it.  Is this adequate?  If not, we might
#     have to have a bunch of subclasses GNUCCompiler, SGICCompiler,
#     SunCCompiler, and I suspect down that road lies madness.
#   * even if we don't know a warning flag from an optimization flag,
#     we need some way for outsiders to feed preprocessor/compiler/linker
#     flags in to us -- eg. a sysadmin might want to mandate certain flags
#     via a site config file, or a user might want to set something for
#     compiling this module distribution only via the setup.py command
#     line, whatever.  As long as these options come from something on the
#     current system, they can be as system-dependent as they like, and we
#     should just happily stuff them into the preprocessor/compiler/linker
#     options and carry on.

def _split_env(cmd):
    """
    For macOS, split command into 'env' portion (if any)
    and the rest of the linker command.

    >>> _split_env(['a', 'b', 'c'])
    ([], ['a', 'b', 'c'])
    >>> _split_env(['/usr/bin/env', 'A=3', 'gcc'])
    (['/usr/bin/env', 'A=3'], ['gcc'])
    """
    pivot = 0
    if os.path.basename(cmd[0]) == "env":
        pivot = 1
        while '=' in cmd[pivot]:
            pivot += 1
    return cmd[:pivot], cmd[pivot:]

def _split_aix(cmd):
    """
    AIX platforms prefix the compiler with the ld_so_aix
    script, so split that from the linker command.

    >>> _split_aix(['a', 'b', 'c'])
    ([], ['a', 'b', 'c'])
    >>> _split_aix(['/bin/foo/ld_so_aix', 'gcc'])
    (['/bin/foo/ld_so_aix'], ['gcc'])
    """
    pivot = os.path.basename(cmd[0]) == 'ld_so_aix'
    return cmd[:pivot], cmd[pivot:]

def _linker_params(linker_cmd, compiler_cmd):
    """
    The linker command usually begins with the compiler
    command (possibly multiple elements), followed by zero or more
    params for shared library building.

    If the LDSHARED env variable overrides the linker command,
    however, the commands may not match.

    Return the best guess of the linker parameters by stripping
    the linker command. If the compiler command does not
    match the linker command, assume the linker command is
    just the first element.

    >>> _linker_params('gcc foo bar'.split(), ['gcc'])
    ['foo', 'bar']
    >>> _linker_params('gcc foo bar'.split(), ['other'])
    ['foo', 'bar']
    >>> _linker_params('ccache gcc foo bar'.split(), 'ccache gcc'.split())
    ['foo', 'bar']
    >>> _linker_params(['gcc'], ['gcc'])
    []
    """
    c_len = len(compiler_cmd)
    pivot = c_len if linker_cmd[:c_len] == compiler_cmd else 1
    return linker_cmd[pivot:]

class UnixCCompiler(CCompiler):

    compiler_type = 'unix'

    # These are used by CCompiler in two places: the constructor sets
    # instance attributes 'preprocessor', 'compiler', etc. from them, and
    # 'set_executable()' allows any of these to be set.  The defaults here
    # are pretty generic; they will probably have to be set by an outsider
    # (eg. using information discovered by the sysconfig about building
    # Python extensions).
    executables = {
        'preprocessor': None,
        'compiler': ["cc"],
        'compiler_so': ["cc"],
        'compiler_cxx': ["cc"],
        'linker_so': ["cc", "-shared"],
        'linker_exe': ["cc"],
        'archiver': ["ar", "-cr"],
        'ranlib': None,
    }

    if sys.platform[:6] == "darwin":
        executables['ranlib'] = ["ranlib"]

    # Needed for the filename generation methods provided by the base
    # class, CCompiler.  NB. whoever instantiates/uses a particular
    # UnixCCompiler instance should set 'shared_lib_ext' -- we set a
    # reasonable common default here, but it's not necessarily used on all
    # Unices!

    src_extensions = [".c", ".C", ".cc", ".cxx", ".cpp", ".m"]
    obj_extension = ".o"
    static_lib_extension = ".a"
    shared_lib_extension = ".so"
    dylib_lib_extension = ".dylib"
    xcode_stub_lib_extension = ".tbd"
    static_lib_format = shared_lib_format = dylib_lib_format = "lib%s%s"
    xcode_stub_lib_format = dylib_lib_format
    if sys.platform == "cygwin":
        exe_extension = ".exe"

    def preprocess(
        self,
        source,
        output_file=None,
        macros=None,
        include_dirs=None,
        extra_preargs=None,
        extra_postargs=None,
    ):
        fixed_args = self._fix_compile_args(None, macros, include_dirs)
        ignore, macros, include_dirs = fixed_args
        pp_opts = gen_preprocess_options(macros, include_dirs)
        pp_args = self.preprocessor + pp_opts
        if output_file:
            pp_args.extend(['-o', output_file])
        if extra_preargs:
            pp_args[:0] = extra_preargs
        if extra_postargs:
            pp_args.extend(extra_postargs)
        pp_args.append(source)

        # reasons to preprocess:
        # - force is indicated
        # - output is directed to stdout
        # - source file is newer than the target
        preprocess = self.force or output_file is None or newer(source, output_file)
        if not preprocess:
            return

        if output_file:
            self.mkpath(os.path.dirname(output_file))

        try:
            self.spawn(pp_args)
        except DistutilsExecError as msg:
            raise CompileError(msg)

    def _compile(self, obj, src, ext, cc_args, extra_postargs, pp_opts):
        compiler_so = compiler_fixup(self.compiler_so, cc_args + extra_postargs)
        try:
            self.spawn(compiler_so + cc_args + [src, '-o', obj] + extra_postargs)
        except DistutilsExecError as msg:
            raise CompileError(msg)

    def create_static_lib(
        self, objects, output_libname, output_dir=None, debug=0, target_lang=None
    ):
        objects, output_dir = self._fix_object_args(objects, output_dir)

        output_filename = self.library_filename(output_libname, output_dir=output_dir)

        if self._need_link(objects, output_filename):
            self.mkpath(os.path.dirname(output_filename))
            self.spawn(self.archiver + [output_filename] + objects + self.objects)

            # Not many Unices required ranlib anymore -- SunOS 4.x is, I
            # think the only major Unix that does.  Maybe we need some
            # platform intelligence here to skip ranlib if it's not
            # needed -- or maybe Python's configure script took care of
            # it for us, hence the check for leading colon.
            if self.ranlib:
                try:
                    self.spawn(self.ranlib + [output_filename])
                except DistutilsExecError as msg:
                    raise LibError(msg)
        else:
            log.debug("skipping %s (up-to-date)", output_filename)

    def link(
        self,
        target_desc,
        objects,
        output_filename,
        output_dir=None,
        libraries=None,
        library_dirs=None,
        runtime_library_dirs=None,
        export_symbols=None,
        debug=0,
        extra_preargs=None,
        extra_postargs=None,
        build_temp=None,
        target_lang=None,
    ):
        objects, output_dir = self._fix_object_args(objects, output_dir)
        fixed_args = self._fix_lib_args(libraries, library_dirs, runtime_library_dirs)
        libraries, library_dirs, runtime_library_dirs = fixed_args

        lib_opts = gen_lib_options(self, library_dirs, runtime_library_dirs, libraries)
        if not isinstance(output_dir, (str, type(None))):
            raise TypeError("'output_dir' must be a string or None")
        if output_dir is not None:
            output_filename = os.path.join(output_dir, output_filename)

        if self._need_link(objects, output_filename):
            ld_args = objects + self.objects + lib_opts + ['-o', output_filename]
            if debug:
                ld_args[:0] = ['-g']
            if extra_preargs:
                ld_args[:0] = extra_preargs
            if extra_postargs:
                ld_args.extend(extra_postargs)
            self.mkpath(os.path.dirname(output_filename))
            try:
                # Select a linker based on context: linker_exe when
                # building an executable or linker_so (with shared options)
                # when building a shared library.
                building_exe = target_desc == CCompiler.EXECUTABLE
                linker = (self.linker_exe if building_exe else self.linker_so)[:]

                if target_lang == "c++" and self.compiler_cxx:
                    env, linker_ne = _split_env(linker)
                    aix, linker_na = _split_aix(linker_ne)
                    _, compiler_cxx_ne = _split_env(self.compiler_cxx)
                    _, linker_exe_ne = _split_env(self.linker_exe)

                    params = _linker_params(linker_na, linker_exe_ne)
                    linker = env + aix + compiler_cxx_ne + params

                linker = compiler_fixup(linker, ld_args)

                self.spawn(linker + ld_args)
            except DistutilsExecError as msg:
                raise LinkError(msg)
        else:
            log.debug("skipping %s (up-to-date)", output_filename)

    # -- Miscellaneous methods -----------------------------------------
    # These are all used by the 'gen_lib_options() function, in
    # ccompiler.py.

    def library_dir_option(self, dir):
        return "-L" + dir

    def _is_gcc(self):
        cc_var = sysconfig.get_config_var("CC")
        compiler = os.path.basename(shlex.split(cc_var)[0])
        return "gcc" in compiler or "g++" in compiler

    def runtime_library_dir_option(self, dir):
        # XXX Hackish, at the very least.  See Python bug #445902:
        # http://sourceforge.net/tracker/index.php
        #   ?func=detail&aid=445902&group_id=5470&atid=105470
        # Linkers on different platforms need different options to
        # specify that directories need to be added to the list of
        # directories searched for dependencies when a dynamic library
        # is sought.  GCC on GNU systems (Linux, FreeBSD, ...) has to
        # be told to pass the -R option through to the linker, whereas
        # other compilers and gcc on other systems just know this.
        # Other compilers may need something slightly different.  At
        # this time, there's no way to determine this information from
        # the configuration data stored in the Python installation, so
        # we use this hack.
        if sys.platform[:6] == "darwin":
            from distutils.util import get_macosx_target_ver, split_version

            macosx_target_ver = get_macosx_target_ver()
            if macosx_target_ver and split_version(macosx_target_ver) >= [10, 5]:
                return "-Wl,-rpath," + dir
            else:  # no support for -rpath on earlier macOS versions
                return "-L" + dir
        elif sys.platform[:7] == "freebsd":
            return "-Wl,-rpath=" + dir
        elif sys.platform[:5] == "hp-ux":
            return [
                "-Wl,+s" if self._is_gcc() else "+s",
                "-L" + dir,
            ]

        # For all compilers, `-Wl` is the presumed way to
        # pass a compiler option to the linker and `-R` is
        # the way to pass an RPATH.
        if sysconfig.get_config_var("GNULD") == "yes":
            # GNU ld needs an extra option to get a RUNPATH
            # instead of just an RPATH.
            return "-Wl,--enable-new-dtags,-R" + dir
        else:
            return "-Wl,-R" + dir

    def library_option(self, lib):
        return "-l" + lib

    @staticmethod
    def _library_root(dir):
        """
        macOS users can specify an alternate SDK using'-isysroot'.
        Calculate the SDK root if it is specified.

        Note that, as of Xcode 7, Apple SDKs may contain textual stub
        libraries with .tbd extensions rather than the normal .dylib
        shared libraries installed in /.  The Apple compiler tool
        chain handles this transparently but it can cause problems
        for programs that are being built with an SDK and searching
        for specific libraries.  Callers of find_library_file need to
        keep in mind that the base filename of the returned SDK library
        file might have a different extension from that of the library
        file installed on the running system, for example:
          /Applications/Xcode.app/Contents/Developer/Platforms/
              MacOSX.platform/Developer/SDKs/MacOSX10.11.sdk/
              usr/lib/libedit.tbd
        vs
          /usr/lib/libedit.dylib
        """
        if sys.platform != 'darwin' or not (
            dir.startswith('/System/')
            or (dir.startswith('/usr/') and not dir.startswith('/usr/local/'))
        ):
            return dir

        cflags = sysconfig.get_config_var('CFLAGS')
        m = re.search(r'-isysroot\s*(\S+)', cflags)
        if m is None:
            sysroot = '/'
        else:
            sysroot = m.group(1)

        return os.path.join(sysroot, dir[1:])

    def find_library_file(self, dirs, lib, debug=0):
        shared_f = self.library_filename(lib, lib_type='shared')
        dylib_f = self.library_filename(lib, lib_type='dylib')
        xcode_stub_f = self.library_filename(lib, lib_type='xcode_stub')
        static_f = self.library_filename(lib, lib_type='static')

        for dir in dirs:
            root = self._library_root(dir)
            shared = os.path.join(root, shared_f)
            dylib = os.path.join(root, dylib_f)
            static = os.path.join(root, static_f)
            xcode_stub = os.path.join(root, xcode_stub_f)

            # We're second-guessing the linker here, with not much hard
            # data to go on: GCC seems to prefer the shared library, so I'm
            # assuming that *all* Unix C compilers do.  And of course I'm
            # ignoring even GCC's "-static" option.  So sue me.
            if os.path.exists(dylib):
                return dylib
            elif os.path.exists(xcode_stub):
                return xcode_stub
            elif os.path.exists(shared):
                return shared
            elif os.path.exists(static):
                return static

        # Oops, didn't find it in *any* of 'dirs'
        return None
<|endoftext|>


<commit_before>"Functions that help with dynamically creating decorators for views."

import types
try:
    from functools import wraps, update_wrapper
except ImportError:
    from django.utils.functional import wraps, update_wrapper  # Python 2.3, 2.4 fallback.

class MethodDecoratorAdaptor(object):
    """
    Generic way of creating decorators that adapt to being
    used on methods
    """
    def __init__(self, decorator, func):
        update_wrapper(self, func)
        # NB: update the __dict__ first, *then* set
        # our own .func and .decorator, in case 'func' is actually
        # another MethodDecoratorAdaptor object, which has its
        # 'func' and 'decorator' attributes in its own __dict__
        self.decorator = decorator
        self.func = func
    def __call__(self, *args, **kwargs):
        return self.decorator(self.func)(*args, **kwargs)
    def __get__(self, instance, owner):
        return self.decorator(self.func.__get__(instance, owner))
    def _get_name(self):
        return self.func.__name__
    def _get_doc(self):
        return self.func.__doc__

def auto_adapt_to_methods(decorator):
    """Allows you to use the same decorator on methods and functions,
    hiding the self argument from the decorator."""
    def adapt(func):
        return MethodDecoratorAdaptor(decorator, func)
    return wraps(decorator)(adapt)

def decorator_from_middleware_with_args(middleware_class):
    """
    Like decorator_from_middleware, but returns a function
    that accepts the arguments to be passed to the middleware_class.
    Use like::

         cache_page = decorator_from_middleware(CacheMiddleware)
         # ...

         @cache_page(3600)
         def my_view(request):
             # ...
    """
    return make_middleware_decorator(middleware_class)

def decorator_from_middleware(middleware_class):
    """
    Given a middleware class (not an instance), returns a view decorator. This
    lets you use middleware functionality on a per-view basis. The middleware
    is created with no params passed.
    """
    return make_middleware_decorator(middleware_class)()

def make_middleware_decorator(middleware_class):
    def _make_decorator(*m_args, **m_kwargs):
        middleware = middleware_class(*m_args, **m_kwargs)
        def _decorator(view_func):
            def _wrapped_view(request, *args, **kwargs):
                if hasattr(middleware, 'process_request'):
                    result = middleware.process_request(request)
                    if result is not None:
                        return result
                if hasattr(middleware, 'process_view'):
                    result = middleware.process_view(request, view_func, args, kwargs)
                    if result is not None:
                        return result
                try:
                    response = view_func(request, *args, **kwargs)
                except Exception, e:
                    if hasattr(middleware, 'process_exception'):
                        result = middleware.process_exception(request, e)
                        if result is not None:
                            return result
                    raise
                if hasattr(middleware, 'process_response'):
                    result = middleware.process_response(request, response)
                    if result is not None:
                        return result
                return response
            return wraps(view_func)(_wrapped_view)
        return auto_adapt_to_methods(_decorator)
    return _make_decorator
<commit_msg>Fixed typo in docstring for decorator_from_middleware_with_args.<commit_after>"Functions that help with dynamically creating decorators for views."

import types
try:
    from functools import wraps, update_wrapper
except ImportError:
    from django.utils.functional import wraps, update_wrapper  # Python 2.3, 2.4 fallback.

class MethodDecoratorAdaptor(object):
    """
    Generic way of creating decorators that adapt to being
    used on methods
    """
    def __init__(self, decorator, func):
        update_wrapper(self, func)
        # NB: update the __dict__ first, *then* set
        # our own .func and .decorator, in case 'func' is actually
        # another MethodDecoratorAdaptor object, which has its
        # 'func' and 'decorator' attributes in its own __dict__
        self.decorator = decorator
        self.func = func
    def __call__(self, *args, **kwargs):
        return self.decorator(self.func)(*args, **kwargs)
    def __get__(self, instance, owner):
        return self.decorator(self.func.__get__(instance, owner))
    def _get_name(self):
        return self.func.__name__
    def _get_doc(self):
        return self.func.__doc__

def auto_adapt_to_methods(decorator):
    """Allows you to use the same decorator on methods and functions,
    hiding the self argument from the decorator."""
    def adapt(func):
        return MethodDecoratorAdaptor(decorator, func)
    return wraps(decorator)(adapt)

def decorator_from_middleware_with_args(middleware_class):
    """
    Like decorator_from_middleware, but returns a function
    that accepts the arguments to be passed to the middleware_class.
    Use like::

         cache_page = decorator_from_middleware_with_args(CacheMiddleware)
         # ...

         @cache_page(3600)
         def my_view(request):
             # ...
    """
    return make_middleware_decorator(middleware_class)

def decorator_from_middleware(middleware_class):
    """
    Given a middleware class (not an instance), returns a view decorator. This
    lets you use middleware functionality on a per-view basis. The middleware
    is created with no params passed.
    """
    return make_middleware_decorator(middleware_class)()

def make_middleware_decorator(middleware_class):
    def _make_decorator(*m_args, **m_kwargs):
        middleware = middleware_class(*m_args, **m_kwargs)
        def _decorator(view_func):
            def _wrapped_view(request, *args, **kwargs):
                if hasattr(middleware, 'process_request'):
                    result = middleware.process_request(request)
                    if result is not None:
                        return result
                if hasattr(middleware, 'process_view'):
                    result = middleware.process_view(request, view_func, args, kwargs)
                    if result is not None:
                        return result
                try:
                    response = view_func(request, *args, **kwargs)
                except Exception, e:
                    if hasattr(middleware, 'process_exception'):
                        result = middleware.process_exception(request, e)
                        if result is not None:
                            return result
                    raise
                if hasattr(middleware, 'process_response'):
                    result = middleware.process_response(request, response)
                    if result is not None:
                        return result
                return response
            return wraps(view_func)(_wrapped_view)
        return auto_adapt_to_methods(_decorator)
    return _make_decorator
<|endoftext|>


<commit_before>import os

from django.dispatch import receiver
from django.db.models.signals import post_save
from django.contrib.auth.models import User

from intercom import intercom
import requests

from xmodule.modulestore.django import modulestore
from student.models import CourseEnrollment
from course_action_state.models import CourseRerunState

# try:
#     from course_creators.models import CourseCreator
# except ImportError:
#     # `course_creators` app is included only in the Studio
#     CourseCreator = None

intercom.Intercom.app_id = os.environ.get("INTERCOM_APP_ID", "")
intercom.Intercom.api_key = os.environ.get("INTERCOM_APP_SECRET", "")
INTERCOM_USER_EMAIL = os.environ.get("INTERCOM_USER_EMAIL", "")

def intercom_update(custom_data):
    try:
        intercom.Intercom.update_user(email=INTERCOM_USER_EMAIL, custom_data=custom_data)
    except (intercom.IntercomError, requests.exceptions.RequestException):
        pass

def update_user_statistics():
    custom_data = {
        'Users - total': User.objects.count(),
        'Users - active': User.objects.filter(is_active=True).count(),
        # TODO: Filter out inactive users?
        'Users - students': User.objects.filter(
            courseenrollment__is_active=True).distinct().count(),
    }
    # if CourseCreator:
    #     custom_data['Users - course creation rights granted'] = User.objects.filter(
    #         coursecreator__state=CourseCreator.GRANTED).count()
    intercom_update(custom_data=custom_data)

update_user_statistics()

@receiver(post_save, dispatch_uid='user_save_callback')
def user_save_callback(sender, instance, created, raw, **kwargs):
    if sender in (User, CourseEnrollment):  # CourseCreator
        update_user_statistics()

def update_course_statistics():
    custom_data = {
        # TODO: Figure out whether we need to filter out broken items or not
        #       https://github.com/edx/edx-platform/blob/d4de932c2b46dbe1ad6439731b4312fb36813d6d/cms/djangoapps/contentstore/views/course.py#L281
        'Courses - total': len(modulestore().get_courses()),

        # TODO: There might be some unfinished/broken reruns
        #       https://github.com/appsembler/edx-platform/blob/d4de932c2b46dbe1ad6439731b4312fb36813d6d/cms/djangoapps/contentstore/views/course.py#L295
        'Courses - reruns': CourseRerunState.objects.count(),
    }
    intercom_update(custom_data=custom_data)

update_course_statistics()

@receiver(post_save, sender=CourseRerunState, dispatch_uid='course_save_callback')
def course_save_callback(sender, instance, created, raw, **kwargs):
    if created:
        update_course_statistics()
<commit_msg>Send statistics only if Intercom has been set up<commit_after>import os

from django.dispatch import receiver
from django.db.models.signals import post_save
from django.contrib.auth.models import User

from intercom import intercom
import requests

from xmodule.modulestore.django import modulestore
from student.models import CourseEnrollment
from course_action_state.models import CourseRerunState

# try:
#     from course_creators.models import CourseCreator
# except ImportError:
#     # `course_creators` app is included only in the Studio
#     CourseCreator = None

intercom.Intercom.app_id = os.environ.get("INTERCOM_APP_ID", "")
intercom.Intercom.api_key = os.environ.get("INTERCOM_APP_SECRET", "")
INTERCOM_USER_EMAIL = os.environ.get("INTERCOM_USER_EMAIL", "")

def intercom_update(custom_data):
    try:
        intercom.Intercom.update_user(email=INTERCOM_USER_EMAIL, custom_data=custom_data)
    except (intercom.IntercomError, requests.exceptions.RequestException):
        pass

def update_user_statistics():
    if not intercom.Intercom.app_id:
        return
    custom_data = {
        'Users - total': User.objects.count(),
        'Users - active': User.objects.filter(is_active=True).count(),
        # TODO: Filter out inactive users?
        'Users - students': User.objects.filter(
            courseenrollment__is_active=True).distinct().count(),
    }
    # if CourseCreator:
    #     custom_data['Users - course creation rights granted'] = User.objects.filter(
    #         coursecreator__state=CourseCreator.GRANTED).count()
    intercom_update(custom_data=custom_data)

update_user_statistics()

@receiver(post_save, dispatch_uid='user_save_callback')
def user_save_callback(sender, instance, created, raw, **kwargs):
    if sender in (User, CourseEnrollment):  # CourseCreator
        update_user_statistics()

def update_course_statistics():
    if not intercom.Intercom.app_id:
        return
    custom_data = {
        # TODO: Figure out whether we need to filter out broken items or not
        #       https://github.com/edx/edx-platform/blob/d4de932c2b46dbe1ad6439731b4312fb36813d6d/cms/djangoapps/contentstore/views/course.py#L281
        'Courses - total': len(modulestore().get_courses()),

        # TODO: There might be some unfinished/broken reruns
        #       https://github.com/appsembler/edx-platform/blob/d4de932c2b46dbe1ad6439731b4312fb36813d6d/cms/djangoapps/contentstore/views/course.py#L295
        'Courses - reruns': CourseRerunState.objects.count(),
    }
    intercom_update(custom_data=custom_data)

update_course_statistics()

@receiver(post_save, sender=CourseRerunState, dispatch_uid='course_save_callback')
def course_save_callback(sender, instance, created, raw, **kwargs):
    if created:
        update_course_statistics()
<|endoftext|>


<commit_before>"""Custom Django User models for Stormpath.

Any application that uses django_stormpath must provide a user model with a
url field. The url is used in the authentication backend to keep track which
remote Stormpath user the local user represents. It is meant to be used in an
application that modifies user data on Stormpath. The user classes provided
here are convenient to inherit from in custom user models in applications but
the only requirement is that the user model has an url which is not provided by
the default Django User model and the user model has to be set as the default.

* Example (settings.py):

    AUTH_MODEL_USER = 'django_stormpath.StormpathUser'

"""

from django.db import models
from django.contrib.auth.models import (AbstractUser,
    BaseUserManager, AbstractBaseUser, PermissionsMixin)

class StormpathUserManager(BaseUserManager):

    def create_user(self, email, username, first_name, last_name, password):

        if not email:
            raise ValueError("Users must have an email address")

        if not first_name or not last_name:
            raise ValueError("Users must provide a given name and a surname")

        user = self.model(email=StormpathUserManager.normalize_email(email),
            first_name=first_name, last_name=last_name)

        user.set_password(password)
        user.save(using=self._db)
        return user

    def create_superuser(self, *args):

        user = self.create_user(args)
        user.is_admin = True
        user.is_staff = True
        user.is_superuser = True
        user.save(using=self._db)
        return user

class StormpathBaseUser(AbstractBaseUser, PermissionsMixin):

    url = models.CharField(max_length=255, null=True, blank=True)
    username = models.CharField(max_length=255)
    first_name = models.CharField(max_length=255)
    last_name = models.CharField(max_length=255)
    email = models.EmailField(verbose_name='email address',
        max_length=255,
        unique=True,
        db_index=True)

    USERNAME_FIELD = 'username'
    REQUIRED_FIELDS = ['email', 'first_name', 'last_name']

    is_active = models.BooleanField(default=True)
    is_admin = models.BooleanField(default=False)
    is_staff = models.BooleanField(default=False)

    objects = StormpathUserManager()

    def get_full_name(self):
        return "{0} {1}".format(self.first_name, self.last_name)

    def get_short_name(self):
        return self.email

    def __unicode__(self):
        return self.get_full_name()

class StormpathUser(AbstractUser):
    url = models.CharField(max_length=255, null=True, blank=True)
<commit_msg>StormpathBaseUser should be abstract model<commit_after>"""Custom Django User models for Stormpath.

Any application that uses django_stormpath must provide a user model with a
url field. The url is used in the authentication backend to keep track which
remote Stormpath user the local user represents. It is meant to be used in an
application that modifies user data on Stormpath. The user classes provided
here are convenient to inherit from in custom user models in applications but
the only requirement is that the user model has an url which is not provided by
the default Django User model and the user model has to be set as the default.

* Example (settings.py):

    AUTH_MODEL_USER = 'django_stormpath.StormpathUser'

"""

from django.db import models
from django.contrib.auth.models import (AbstractUser,
    BaseUserManager, AbstractBaseUser, PermissionsMixin)

class StormpathUserManager(BaseUserManager):

    def create_user(self, email, username, first_name, last_name, password):

        if not email:
            raise ValueError("Users must have an email address")

        if not first_name or not last_name:
            raise ValueError("Users must provide a given name and a surname")

        user = self.model(email=StormpathUserManager.normalize_email(email),
            first_name=first_name, last_name=last_name)

        user.set_password(password)
        user.save(using=self._db)
        return user

    def create_superuser(self, *args):

        user = self.create_user(args)
        user.is_admin = True
        user.is_staff = True
        user.is_superuser = True
        user.save(using=self._db)
        return user

class StormpathBaseUser(AbstractBaseUser, PermissionsMixin):

    class Meta:
        abstract = True

    url = models.CharField(max_length=255, null=True, blank=True)
    username = models.CharField(max_length=255)
    first_name = models.CharField(max_length=255)
    last_name = models.CharField(max_length=255)
    email = models.EmailField(verbose_name='email address',
        max_length=255,
        unique=True,
        db_index=True)

    USERNAME_FIELD = 'username'
    REQUIRED_FIELDS = ['email', 'first_name', 'last_name']

    is_active = models.BooleanField(default=True)
    is_admin = models.BooleanField(default=False)
    is_staff = models.BooleanField(default=False)

    objects = StormpathUserManager()

    def get_full_name(self):
        return "{0} {1}".format(self.first_name, self.last_name)

    def get_short_name(self):
        return self.email

    def __unicode__(self):
        return self.get_full_name()

class StormpathUser(AbstractUser):
    url = models.CharField(max_length=255, null=True, blank=True)
<|endoftext|>


<commit_before>__version__ = '0.1.16DEV'
__author__ = 'benzkji'
<commit_msg>0.1.16<commit_after>__version__ = '0.1.16'
__author__ = 'benzkji'
<|endoftext|>


<commit_before>from django.contrib.auth.models import User
from django.http import HttpResponseRedirect
from django.core.urlresolvers import reverse_lazy
from django.core.exceptions import PermissionDenied
from django.contrib.auth.decorators import user_passes_test
from django.utils.decorators import available_attrs
from django.http import HttpResponseRedirect
from django.utils.encoding import force_str
from django.utils.functional import wraps
from django.shortcuts import resolve_url
from django.contrib.auth import login

from djzbar.utils.mssql import get_userid
from djtools.utils.users import in_group

def portal_auth_required(group=None, redirect_url=None):
    """
    Restrict access to a view that receives UID from the portal
    """

    def decorator(view_func):
        @wraps(view_func, assigned=available_attrs(view_func))
        def _wrapped_view(request, *args, **kwargs):
            response = view_func(request, *args, **kwargs)
            resolved_redirect_url = force_str(
                resolve_url(redirect_url or reverse_lazy("auth_login"))
            )
            if not request.session.get("BusinessOfficeFinance"):
                if not request.user.is_authenticated():
                    # coming from the portal with uid set?
                    uid = get_userid(request.GET.get('uid'))
                    try:
                        user = User.objects.get(pk=uid)
                    except:
                        # nope
                        return HttpResponseRedirect(reverse_lazy("auth_login"))
                else:
                    user = request.user
                if group:
                    if not in_group(user, group) and not user.is_superuser:
                        return HttpResponseRedirect(resolved_redirect_url)
                # sign in the user manually
                user.backend = 'django.contrib.auth.backends.ModelBackend'
                login(request, user)
                request.session["BusinessOfficeFinance"] = True
            return view_func(request, *args, **kwargs)
        return _wrapped_view
    return decorator

def superuser_only(function):
    """
    Limit view to superusers only.
    Usage:
    --------------------------------------------------------------------------
    @superuser_only
    def my_view(request):
        ...
    --------------------------------------------------------------------------
    or in urls:
    --------------------------------------------------------------------------
    urlpatterns = patterns('',
        (r'^foobar/(.*)', superuser_only(my_view)),
    )
    --------------------------------------------------------------------------
    """
    def _inner(request, *args, **kwargs):
        if not request.user.is_superuser:
            #raise PermissionDenied
            return HttpResponseRedirect(reverse_lazy("auth_login"))
        return function(request, *args, **kwargs)
    return _inner

def group_required(*group_names):
    """
    Requires user membership in at least one of the groups passed in:

    @group_required('admins','editors')
    def myview(request, id):

    """
    def in_groups(u):
        if u.is_authenticated():
            if u.is_superuser or bool(u.groups.filter(name__in=group_names)):
                return True
        return False
    return user_passes_test(in_groups)
<commit_msg>fixed portal required decorator but not certain how; i think the use of 'decorator' in the name of one of the functions might have been problematic. not certain but now it works without sending a POST request twice<commit_after>from django.contrib.auth.models import User
from django.http import HttpResponseRedirect
from django.core.urlresolvers import reverse_lazy
from django.core.exceptions import PermissionDenied
from django.contrib.auth.decorators import user_passes_test
from django.utils.decorators import available_attrs
from django.http import HttpResponseRedirect
from django.utils.encoding import force_str
#from django.utils.functional import wraps
from django.shortcuts import resolve_url
from django.contrib.auth import login

from djzbar.utils.mssql import get_userid
from djtools.utils.users import in_group

from functools import wraps

def portal_auth_required(session_var, group=None, redirect_url=None):
    def _portal_auth_required(view_func):
        @wraps(view_func, assigned=available_attrs(view_func))
        def wrapper(request, *args, **kwargs):
            resolved_redirect_url = force_str(
                resolve_url(redirect_url or reverse_lazy("auth_login"))
            )
            if not request.session.get(session_var):
                if not request.user.is_authenticated():
                    # coming from the portal with uid set?
                    uid = request.GET.get('uid')
                    if uid:
                        # do we have a user id or portal id?
                        if len(uid) > 12:
                            uid = get_userid(request.GET.get('uid'))
                        try:
                            user = User.objects.get(pk=uid.strip("0"))
                        except:
                            # nope
                            return HttpResponseRedirect(reverse_lazy("auth_login"))
                    else:
                        return HttpResponseRedirect(reverse_lazy("auth_login"))
                else:
                    user = request.user
                if group:
                    if not in_group(user, group) and not user.is_superuser:
                        return HttpResponseRedirect(resolved_redirect_url)
                # sign in the user manually
                user.backend = 'django.contrib.auth.backends.ModelBackend'
                login(request, user)
                request.session[session_var] = True

            return view_func(request, *args, **kwargs)
        return wrapper
    return _portal_auth_required

def superuser_only(function):
    """
    Limit view to superusers only.
    Usage:
    --------------------------------------------------------------------------
    @superuser_only
    def my_view(request):
        ...
    --------------------------------------------------------------------------
    or in urls:
    --------------------------------------------------------------------------
    urlpatterns = patterns('',
        (r'^foobar/(.*)', superuser_only(my_view)),
    )
    --------------------------------------------------------------------------
    """
    def _inner(request, *args, **kwargs):
        if not request.user.is_superuser:
            #raise PermissionDenied
            return HttpResponseRedirect(reverse_lazy("auth_login"))
        return function(request, *args, **kwargs)
    return _inner

def group_required(*group_names):
    """
    Requires user membership in at least one of the groups passed in:

    @group_required('admins','editors')
    def myview(request, id):

    """
    def in_groups(u):
        if u.is_authenticated():
            if u.is_superuser or bool(u.groups.filter(name__in=group_names)):
                return True
        return False
    return user_passes_test(in_groups)
<|endoftext|>