def resolve_report(self, db: Session, *, user: models.User,
                       db_obj: models.Fact) -> models.Fact:
        db.query(models.Reported) \
            .filter(models.Reported.fact_id == db_obj.fact_id).delete(
            synchronize_session=False)
        db.commit()

        history_in = schemas.HistoryCreate(
            time=datetime.now(timezone('UTC')).isoformat(),
            user_id=user.id,
            fact_id=db_obj.fact_id,
            log_type=schemas.Log.resolve_report,
            details={"study_system": user.repetition_model})
        crud.history.create(db=db, obj_in=history_in)
        return db_obj
    def undo_mark(self, db: Session, *, db_obj: models.Fact,
                  user: models.User) -> models.Fact:
        db.query(models.Marked) \
            .filter(and_(models.Marked.marked_fact == db_obj,
                         models.Marked.marker == user)).delete(synchronize_session=False)
        db.commit()

        history_in = schemas.HistoryCreate(
            time=datetime.now(timezone('UTC')).isoformat(),
            user_id=user.id,
            fact_id=db_obj.fact_id,
            log_type=schemas.Log.undo_mark,
            details={"study_system": user.repetition_model})
        crud.history.create(db=db, obj_in=history_in)
        return db_obj
    def mark(self, db: Session, *, db_obj: models.Fact,
             user: models.User) -> models.Fact:
        now = datetime.now(timezone('UTC'))

        mark = models.Marked(marker=user, marked_fact=db_obj, date_marked=now)
        db.add(mark)
        db.commit()

        history_in = schemas.HistoryCreate(
            time=now,
            user_id=user.id,
            fact_id=db_obj.fact_id,
            log_type=schemas.Log.mark,
            details={"study_system": user.repetition_model})
        crud.history.create(db=db, obj_in=history_in)
        return db_obj
    def report(self, db: Session, *, db_obj: models.Fact, user: models.User,
               suggestion: schemas.FactToReport) -> models.Fact:
        now = datetime.now(timezone('UTC'))
        report = models.Reported(reporter=user,
                                 reported_fact=db_obj,
                                 date_reported=datetime.now(timezone('UTC')),
                                 suggestion=suggestion)
        db.add(report)
        db.commit()

        history_in = schemas.HistoryCreate(
            time=now,
            user_id=user.id,
            fact_id=db_obj.fact_id,
            log_type=schemas.Log.report,
            details={"study_system": user.repetition_model})
        crud.history.create(db=db, obj_in=history_in)
        return db_obj
def assign_decks(
    *,
    db: Session = Depends(deps.get_db),
    deck_ids: List[int] = Query(...),
    current_user: models.User = Depends(deps.get_current_active_user),
) -> Any:
    """
        Assign an existing deck to a new user.
    """

    decks = []
    for deck_id in deck_ids:
        deck = crud.deck.get(db=db, id=deck_id)
        if not deck:
            raise HTTPException(status_code=404, detail="Deck not found")

        if deck.public:
            if deck not in current_user.decks:
                deck = crud.deck.assign_viewer(db=db,
                                               db_obj=deck,
                                               user=current_user)
                decks.append(deck)
        else:
            raise HTTPException(
                status_code=401,
                detail=
                "User does not have permission to add one of the specified "
                "decks")

    history_in = schemas.HistoryCreate(
        time=datetime.now(timezone('UTC')).isoformat(),
        user_id=current_user.id,
        log_type=schemas.Log.assign_viewer,
        details={
            "study_system": current_user.repetition_model,
            "decks": deck_ids
        })
    crud.history.create(db=db, obj_in=history_in)
    return decks
def update_fact(
        *,
        fact_in: schemas.FactUpdate,
        perms: deps.OwnerFactPerms = Depends(),
) -> Any:
    """
    Update a fact.
    """
    details = {
        "old_fact": perms.fact.__dict__,
        "fact_update": fact_in.dict(),
    }

    fact = crud.fact.update(db=perms.db, db_obj=perms.fact, obj_in=fact_in)

    history_in = schemas.HistoryCreate(time=datetime.now(
        timezone('UTC')).isoformat(),
                                       user_id=perms.current_user.id,
                                       log_type=schemas.Log.update_fact,
                                       details=details)
    crud.history.create(db=perms.db, obj_in=history_in)
    return fact
def read_facts(
    db: Session = Depends(deps.get_db),
    skip: int = 0,
    limit: int = 100,
    all: Optional[str] = None,
    text: Optional[str] = None,
    answer: Optional[str] = None,
    category: Optional[str] = None,
    identifier: Optional[str] = None,
    deck_ids: Optional[List[int]] = Query(None),
    deck_id: Optional[int] = None,
    marked: Optional[bool] = None,
    suspended: Optional[bool] = None,
    reported: Optional[bool] = None,
    current_user: models.User = Depends(deps.get_current_active_user),
) -> Any:
    """
    Retrieve facts.
    """
    if limit > 1000:
        raise HTTPException(
            status_code=445,
            detail="Too many facts requested. Please limit to <1000 facts.")
    if deck_ids is not None and 2 in deck_ids:
        raise HTTPException(status_code=557,
                            detail="This deck is currently unavailable")

    if suspended and reported:
        studyable = True
    else:
        studyable = False
    search = schemas.FactSearch(all=all,
                                text=text,
                                answer=answer,
                                category=category,
                                identifier=identifier,
                                deck_ids=deck_ids,
                                deck_id=deck_id,
                                marked=marked,
                                suspended=suspended,
                                reported=reported,
                                studyable=studyable,
                                skip=skip,
                                limit=limit)
    query = crud.fact.build_facts_query(db=db,
                                        user=current_user,
                                        filters=search)
    facts = crud.fact.get_eligible_facts(query=query, skip=skip, limit=limit)
    total = len(facts)
    if total == limit:
        total = crud.fact.count_eligible_facts(query=query)

    begin_overall_start = time.time()
    new_facts: List[schemas.Fact] = []
    for fact in facts:
        new_facts.append(
            crud.fact.get_schema_with_perm(db_obj=fact, user=current_user))
    overall_end_time = time.time()
    overall_total_time = overall_end_time - begin_overall_start
    logger.info("permissions: " + str(overall_total_time))

    fact_browser = schemas.FactBrowse(facts=new_facts, total=total)
    details = search.dict()
    details["study_system"] = current_user.repetition_model
    history_in = schemas.HistoryCreate(time=datetime.now(
        timezone('UTC')).isoformat(),
                                       user_id=current_user.id,
                                       log_type=schemas.Log.browser,
                                       details=details)
    crud.history.create(db=db, obj_in=history_in)
    return fact_browser
    def update_schedule(
        self, db: Session, *, user: models.User, db_obj: models.Fact,
        schedule: schemas.Schedule
    ) -> Union[bool, requests.exceptions.RequestException,
               json.decoder.JSONDecodeError]:
        try:
            response = schedule.response
            date_studied = datetime.now(timezone('UTC')).isoformat()
            details = {
                "study_system": user.repetition_model,
                "typed": schedule.typed,
                "response": schedule.response,
                "debug_id": schedule.debug_id,
            }
            if schedule.elapsed_seconds_text:
                details["elapsed_seconds_text"] = schedule.elapsed_seconds_text
                details[
                    "elapsed_seconds_answer"] = schedule.elapsed_seconds_answer
            else:
                details[
                    "elapsed_milliseconds_text"] = schedule.elapsed_milliseconds_text
                details[
                    "elapsed_milliseconds_answer"] = schedule.elapsed_milliseconds_answer

            history_in = schemas.HistoryCreate(time=date_studied,
                                               user_id=user.id,
                                               fact_id=db_obj.fact_id,
                                               log_type=schemas.Log.study,
                                               details=details)
            history = crud.history.create(db=db, obj_in=history_in)
            payload_update = [
                schemas.KarlFactUpdate(
                    text=db_obj.text,
                    user_id=user.id,
                    repetition_model=user.repetition_model,
                    fact_id=db_obj.fact_id,
                    history_id=history.id,
                    category=db_obj.category,
                    deck_name=db_obj.deck.title,
                    deck_id=db_obj.deck_id,
                    answer=db_obj.answer,
                    env=settings.ENVIRONMENT,
                    elapsed_seconds_text=schedule.elapsed_seconds_text,
                    elapsed_seconds_answer=schedule.elapsed_seconds_answer,
                    elapsed_milliseconds_text=schedule.
                    elapsed_milliseconds_text,
                    elapsed_milliseconds_answer=schedule.
                    elapsed_milliseconds_answer,
                    label=response,
                    debug_id=schedule.debug_id).dict(exclude_unset=True)
            ]
            logger.info(payload_update[0])
            request = requests.post(settings.INTERFACE + "api/karl/update",
                                    json=payload_update)
            logger.info(request.request)
            if 200 <= request.status_code < 300:
                return True
            else:
                return False
        except requests.exceptions.RequestException as e:
            capture_exception(e)
            return e
        except json.decoder.JSONDecodeError as e:
            capture_exception(e)
            return e
    def get_study_set(
        self,
        db: Session,
        *,
        user: models.User,
        deck_ids: List[int] = None,
        return_limit: Optional[int] = None,
        send_limit: Optional[int] = 300,
    ) -> Union[List[schemas.Fact], requests.exceptions.RequestException,
               json.decoder.JSONDecodeError]:
        filters = schemas.FactSearch(deck_ids=deck_ids,
                                     limit=send_limit,
                                     randomize=True,
                                     studyable=True)
        query = crud.fact.build_facts_query(db=db, user=user, filters=filters)
        eligible_facts = self.get_eligible_facts(query=query, limit=send_limit)
        if not eligible_facts:
            return []
        karl_list = []
        karl_list_start = time.time()
        for each_card in eligible_facts:
            karl_list.append(
                schemas.KarlFact(text=each_card.text,
                                 answer=each_card.answer,
                                 category=each_card.category,
                                 deck_name=each_card.deck.title,
                                 deck_id=each_card.deck_id,
                                 user_id=user.id,
                                 fact_id=each_card.fact_id,
                                 repetition_model=user.repetition_model,
                                 env=settings.ENVIRONMENT).dict())
        eligible_fact_time = time.time() - karl_list_start
        logger.info("eligible fact time: " + str(eligible_fact_time))

        karl_query_start = time.time()
        try:
            scheduler_response = requests.post(settings.INTERFACE +
                                               "api/karl/schedule",
                                               json=karl_list)
            response_json = scheduler_response.json()
            card_order = response_json["order"]
            rationale = response_json["rationale"]
            debug_id = response_json["debug_id"]

            query_time = time.time() - karl_query_start
            logger.info(scheduler_response.request)
            logger.info("query time: " + str(query_time))

            facts = []
            if rationale != "<p>no fact received</p>":
                reordered_karl_list = [karl_list[x] for x in card_order]
                if return_limit:
                    for _, each_karl_fact in zip(range(return_limit),
                                                 reordered_karl_list):
                        retrieved_fact = self.get(
                            db=db, id=int(each_karl_fact["fact_id"]))
                        fact_schema = self.get_schema_with_perm(
                            db_obj=retrieved_fact, user=user)
                        fact_schema.rationale = rationale
                        fact_schema.debug_id = debug_id
                        if retrieved_fact:
                            fact_schema.marked = True if user in retrieved_fact.markers else False
                        facts.append(fact_schema)
                else:
                    for each_karl_fact in reordered_karl_list:
                        retrieved_fact = self.get(
                            db=db, id=int(each_karl_fact["fact_id"]))
                        fact_schema = self.get_schema_with_perm(
                            db_obj=retrieved_fact, user=user)
                        fact_schema.rationale = rationale
                        # MARK: maybe not the most efficient solution for determining if user has marked a fact
                        if retrieved_fact:
                            fact_schema.marked = retrieved_fact.is_marked(user)
                        facts.append(fact_schema)
            details = {
                "study_system": user.repetition_model,
                "first_fact": facts[0] if len(facts) != 0 else "empty",
                "eligible_fact_time": query_time,
                "scheduler_query_time": eligible_fact_time,
                "debug_id": debug_id,
            }
            history_in = schemas.HistoryCreate(time=datetime.now(
                timezone('UTC')).isoformat(),
                                               user_id=user.id,
                                               log_type=schemas.Log.get_facts,
                                               details=details)
            crud.history.create(db=db, obj_in=history_in)
            return facts
        except requests.exceptions.RequestException as e:
            capture_exception(e)
            return e
        except json.decoder.JSONDecodeError as e:
            capture_exception(e)
            return e
async def upload_picture(request: Request,
                         file: UploadFile = File(...),
                         db: Session = Depends(get_db)):
    """
    Upload a user picture. IF picture is valid, the face shape is detected, a new record is added to the pictures and
    history table, and the picture file is saved
    :param request:
    :param file: Selected binary picture file to be uploaded
    :param db: db session instance
    :return: Json response that contains the picture information, the detected face shape and the history record
    """
    user_data = get_user_data_from_token(request)

    if user_data:
        save_path = PICTURE_UPLOAD_FOLDER
        file_name = picture_service.save_picture(file, save_path)
        face_detected = picture_service.detect_face(file_name, save_path)

        if face_detected is True:
            # try to find face landmark points
            face_landmarks = picture_service.detect_face_landmarks(
                file_name, save_path)
            if face_landmarks is None:
                raise HTTPException(status_code=422,
                                    detail="No face landmarks detected")
            else:
                picture_info = picture_service.get_picture_info(
                    save_path, file_name)

                # detect face_shape
                face_shape = picture_service.detect_face_shape(
                    face_landmarks, file_name, save_path)
                if face_shape is None:
                    raise HTTPException(
                        status_code=422,
                        detail="Face shape could not be detected")

                new_picture = models.Picture(file_name=picture_info.file_name,
                                             file_path=picture_info.file_path,
                                             file_size=picture_info.file_size,
                                             height=picture_info.height,
                                             width=picture_info.width)

                orig_pic = picture_actions.add_picture(db=db,
                                                       picture=new_picture)

                # Testing how to create a history record after detecting a face shape
                # create History instance

                # parse face shape string to int
                face_shape_id = face_shape_service.parse_face_shape(
                    face_shape[0])

                face_shape_detected: models.FaceShape = face_shape_actions.get_face_shape(
                    db=db, face_shape_id=face_shape_id)

                user_id = user_data['id']

                new_history: schemas.HistoryCreate = schemas.HistoryCreate(
                    picture_id=orig_pic.id,
                    original_picture_id=orig_pic.id,
                    face_shape_id=face_shape_id,
                    user_id=user_id)

                new_history_entry: models.History = history_actions.add_history(
                    db=db, history=new_history)

                results = picture_actions.read_picture_by_file_name(
                    db=db, file_name=new_picture.file_name, limit=1)
                return {
                    'picture': results[0],
                    'face_shape': face_shape[0],
                    'history_entry': new_history_entry
                }
        else:
            raise HTTPException(status_code=422, detail="No face detected")
    else:
        raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED,
                            detail="Invalid credentials")
async def change_hairstyle(user_picture_id: Optional[int] = None,
                           model_picture_id: Optional[int] = None,
                           user_picture_file_name: Optional[str] = None,
                           model_picture_file_name: Optional[str] = None,
                           db: Session = Depends(get_db)):
    """
    Change user's picture hairstyle based on the selected model picture hairstyle, add record to pictures table and
    history table

    :param user_picture_id: The ID of the user picture to be modified
    :param model_picture_id: the ID of the model picture hairstyle
    :param user_picture_file_name: the user picture filename
    :param model_picture_file_name: the model picture filename
    :param db: db session instance
    :return: history record
    """
    user_picture: Union[models.Picture, None] = None
    model_picture: Union[models.ModelPicture, None] = None

    if user_picture_id and model_picture_id:
        user_picture = picture_actions.read_picture_by_id(
            db, picture_id=user_picture_id)
        model_picture = model_picture_actions.read_model_picture_by_id(
            db, model_picture_id=model_picture_id)

    elif user_picture_file_name and model_picture_file_name:
        picture_results: List[
            models.Picture] = picture_actions.read_picture_by_file_name(
                db=db, file_name=user_picture_file_name, limit=1)
        if len(picture_results):
            user_picture = picture_results[0]
            model_pictures_results = model_picture_actions.read_model_picture_by_file_name(
                db=db, file_name=model_picture_file_name, limit=1)
            if len(model_pictures_results):
                model_picture = model_pictures_results[0]

    else:
        raise HTTPException(
            status_code=400,
            detail=
            'Please enter (user_picture_id AND model_picture_id) OR (user_picture_filename AND '
            'model_picture_filename)')

    if user_picture and model_picture:

        try:
            # get original picture from history based on user_picture
            pic_history: List[
                models.History] = history_actions.get_picture_history(
                    db=db, filename=user_picture.file_name)
            original_pic_id: int = pic_history[0].original_picture_id
            original_pic: models.Picture = picture_actions.read_picture_by_id(
                db=db, picture_id=original_pic_id)

            # apply hair transfer
            picture_info = picture_service.change_hairstyle(
                user_picture=original_pic, model_picture=model_picture)
            print(picture_info)

            # create new picture and add to db
            new_picture: schemas.PictureCreate = schemas.PictureCreate(
                file_name=picture_info.file_name,
                file_path=picture_info.file_path,
                file_size=picture_info.file_size,
                height=picture_info.height,
                width=picture_info.width)

            mod_pic = picture_actions.add_picture(db=db, picture=new_picture)

            user = history_actions.get_user_id_from_picture_id(
                db=db, picture_id=user_picture.id)

            if user:
                # get latest user history entry to add face_shape_id
                history: List[
                    models.History] = history_actions.get_user_history(
                        db=db, user_id=user.id)

                if len(history):
                    latest_entry = history[-1]
                    model_picture: models.ModelPicture = model_picture_actions.read_model_picture_by_id(
                        db=db, model_picture_id=model_picture.id)
                    if model_picture:
                        new_history: schemas.HistoryCreate = schemas.HistoryCreate(
                            picture_id=mod_pic.id,
                            original_picture_id=latest_entry.
                            original_picture_id,
                            previous_picture_id=latest_entry.picture_id,
                            #    hair_colour_id=latest_entry.hair_colour_id,
                            hair_style_id=model_picture.hair_style_id,
                            face_shape_id=latest_entry.face_shape_id,
                            user_id=user.id)

                        history_entry = history_actions.add_history(
                            db=db, history=new_history)
                        new_hair_style = hair_style_actions.get_hair_style(
                            db=db, hair_style_id=history_entry.hair_style_id)

                        current_picture = picture_actions.read_picture_by_id(
                            db=db, picture_id=history_entry.picture_id)
                        original_picture = picture_actions.read_picture_by_id(
                            db=db,
                            picture_id=history_entry.original_picture_id)

                        return {
                            "history_entry": history_entry,
                            "hair_style": new_hair_style,
                            "current_picture": current_picture,
                            "original_picture": original_picture
                        }
                    raise HTTPException(status_code=404,
                                        detail='Model picture not found')
                raise HTTPException(
                    status_code=404,
                    detail=
                    'No history associated with this user was found. Please upload a picture '
                    'first.')
            raise HTTPException(
                status_code=404,
                detail='No user associated with this picture was found')
        except Exception as e:
            raise HTTPException(
                status_code=422,
                detail=
                f'Could not apply hair style swap. Please try another image. Exception: {e}'
            )

    raise HTTPException(
        status_code=404,
        detail=
        'No user picture or model picture associated with these IDs were found'
    )
async def change_hair_colour(picture_id: int,
                             colour: str,
                             r: int,
                             b: int,
                             g: int,
                             db: Session = Depends(get_db)):
    """Applies changes to hair colour based on a base colour name (e.g. hot pink) and RGB values, which vary by lightness

    :param picture_id: ID of the picture to be processed
    :param db: db session instance
    :param colour: Base hair colour name
    :param r: Red channel of the colour
    :param g: Green channel of the colour
    :param b: Blue channel of the colour
    :returns: New picture object and history entry reflecting changes
    """

    selected_picture: Union[models.Picture, None] = None

    if picture_id:
        # get latest entry from history where hair_colour_id == None
        first_history_entry = history_actions.get_first_picture_history_by_id(
            db=db, picture_id=picture_id)

        if first_history_entry:
            pic_history = history_actions.get_picture_history_by_original_picture_id(
                db=db,
                original_picture_id=first_history_entry.original_picture_id)

            if len(pic_history):
                latest_entry_no_colour = list(
                    filter(lambda h: not h.hair_colour_id, pic_history))[-1]
                selected_picture = picture_actions.read_picture_by_id(
                    db=db, picture_id=latest_entry_no_colour.picture_id)
    else:
        raise HTTPException(status_code=400, detail='Picture ID not found')

    if selected_picture:
        # apply hair colour
        try:
            picture_info = picture_service.change_hair_colour_RGB(
                file_name=selected_picture.file_name,
                r=r,
                b=b,
                g=g,
                file_path=selected_picture.file_path)
            # create new picture and add to db
            new_picture = models.Picture(file_name=picture_info.file_name,
                                         file_path=picture_info.file_path,
                                         file_size=picture_info.file_size,
                                         height=picture_info.height,
                                         width=picture_info.width)

            mod_pic = picture_actions.add_picture(db=db, picture=new_picture)

            # selected_picture = picture_actions.read_picture_by_id(db, picture_id=picture_id)
            # print(selected_picture.file_name)
            # print(selected_picture.file_path)

            # apply selected colour
            # picture_info = picture_service.change_hair_colour_RGB(file_name=selected_picture.file_name, selected_colour=colour,
            #                                                    r=r, g=g, b=b,
            #                                                    file_path=selected_picture.file_path)
            # print(picture_info)

            # create new picture and add to db
            # new_picture = models.Picture(file_name=picture_info.file_name, file_path=picture_info.file_path,
            #                              file_size=picture_info.file_size, height=picture_info.height, width=picture_info.width)

            # mod_pic = picture_actions.add_picture(db=db, picture=new_picture)

            # fake user_id
            user: models.User = history_actions.get_user_id_from_picture_id(
                db=db, picture_id=picture_id)

            if user:
                hair_colour_results: List[
                    models.HairColour] = hair_colour_actions.get_hair_colours(
                        db=db, search=colour, limit=1)
                if len(hair_colour_results):
                    hair_colour = hair_colour_results[0]
                    # get latest history entry to extract face_shape_id and hair_style_id
                    user_history: List[
                        models.History] = history_actions.get_user_history(
                            db=db, user_id=user.id)
                    latest_history_entry: models.History = user_history[-1]

                    new_history: schemas.HistoryCreate = schemas.HistoryCreate(
                        picture_id=mod_pic.id,
                        original_picture_id=latest_history_entry.
                        original_picture_id,
                        previous_picture_id=selected_picture.id,
                        hair_colour_id=hair_colour.id,
                        face_shape_id=latest_history_entry.face_shape_id,
                        hair_style_id=latest_history_entry.hair_style_id,
                        user_id=user.id)

                    history_entry: models.History = history_actions.add_history(
                        db=db, history=new_history)

                    hair_colour_entry: models.HairColour = hair_colour_actions.get_hair_colour_by_id(
                        db=db, hair_colour_id=hair_colour.id)

                    # get new picture with modified hair colour
                    new_pic: models.Picture = picture_actions.read_picture_by_id(
                        db=db, picture_id=history_entry.picture_id)

                    return {
                        'history_entry': history_entry,
                        'picture': new_pic,
                        'hair_colour': hair_colour_entry
                    }
                raise HTTPException(
                    status_code=404,
                    detail=
                    'No hair colour record associated with this colour name was found'
                )
        except Exception as ex:
            print(ex)
            raise HTTPException(
                status_code=422,
                detail=
                'Could not change hair colour. Please try a different picture. Exception: '
                + ex)
        raise HTTPException(status_code=404,
                            detail='Selected picture not found')
    raise HTTPException(
        status_code=404,
        detail='No user associated with this picture ID was found')