Bnei-Baruch/mdb

View on GitHub
api/auto_series.go

Summary

Maintainability
B
4 hrs
Test Coverage
package api

import (
    "database/sql"
    "errors"
    "fmt"
    "github.com/Bnei-Baruch/mdb/utils"
    "github.com/lib/pq"
    "github.com/volatiletech/sqlboiler/v4/boil"
    "github.com/volatiletech/sqlboiler/v4/queries"
    "github.com/volatiletech/sqlboiler/v4/queries/qm"
    "math"
    "strings"

    "github.com/Bnei-Baruch/mdb/common"
    "github.com/Bnei-Baruch/mdb/events"
    "github.com/Bnei-Baruch/mdb/models"
)

var MinCuNumberForNewLessonSeries = 2
var DaysCheckForLessonsSeries = 30
var TES_ROOT_UID = "xtKmrbb9"
var TES_PARTS_UIDS = []string{"9xNFLSSp", "XlukqLH8", "AerA1hNN", "1kDKQxJb", "o5lXptLo", "eNwJXy4s", "ahipVtPu", "Pscnn3pP", "Lfu7W3CD", "n03vXCJl", "UGcGGSpP", "NpLQT0LX", "AUArdCkH", "tit6XNAo", "FaKUG7ru", "mW6eON0z"}
var ZOAR_UID = "AwGBQX2L"
var ZOAR_PART_ONE_UID = "43BXTx3C"

var qCusByS = fmt.Sprintf(`
  SELECT DISTINCT ON(s.id) s.uid, array_agg(DISTINCT cu.id)
  FROM content_units cu
  INNER JOIN content_units_sources cus ON cu.id = cus.content_unit_id
  INNER JOIN sources s ON s.id = cus.source_id
  WHERE cu.type_id = $1 
  AND coalesce((cu.properties->>'film_date')::date, cu.created_at) > (CURRENT_DATE - '%d day'::interval)
  AND cu.published = TRUE AND cu.secure = 0
  GROUP BY  s.id`, DaysCheckForLessonsSeries)

type AssociateBySources struct {
    tx            boil.Executor
    cu            *models.ContentUnit
    evnts         []events.Event
    seriesSources map[string]bool
    cusByS        map[string][]int64
}

func (a *AssociateBySources) Associate(sUIDs []string) ([]events.Event, error) {
    a.evnts = make([]events.Event, 0)
    a.seriesSources = make(map[string]bool)

    if err := a.prepareCUs(sUIDs); err != nil {
        return nil, NewInternalError(err)
    }
    for sUid, _ := range a.seriesSources {
        if len(a.cusByS[sUid]) < MinCuNumberForNewLessonSeries {
            continue
        }
        c, err := a.findPrevCollection(sUid)
        if errors.Is(err, sql.ErrNoRows) {
            c, err = a.createCollection(sUid)
        }
        if err != nil {
            return nil, NewInternalError(err)
        }

        if err := a.attachCollection(c, sUid); err != nil {
            return nil, NewInternalError(err)
        }
    }
    return a.evnts, nil
}

func (a *AssociateBySources) prepareCUs(cuSUids []string) error {
    rows, err := queries.Raw(qCusByS, common.CONTENT_TYPE_REGISTRY.ByName[common.CT_LESSON_PART].ID).Query(a.tx)
    if err != nil {
        return NewInternalError(err)
    }
    defer rows.Close()

    _cusByS := make(map[string][]int64)
    sUIDs := make([]string, 0)
    var sUid string
    var cuIdsByS pq.Int64Array
    for rows.Next() {
        err = rows.Scan(&sUid, &cuIdsByS)
        if err != nil {
            return NewInternalError(err)
        }
        sUIDs = append(sUIDs, sUid)
        _cusByS[sUid] = append(_cusByS[sUid], cuIdsByS...)
    }
    sUIDs = append(sUIDs, cuSUids...)
    sByLeaf, err := MapParentByLeaf(a.tx, sUIDs)
    if err != nil {
        return NewInternalError(err)
    }
    for _, uid := range cuSUids {
        a.seriesSources[sByLeaf[uid]] = true
    }

    a.cusByS = make(map[string][]int64)
    for _, prevUid := range sUIDs {
        fixedUid := sByLeaf[prevUid]
        for uid, _ := range a.seriesSources {
            if _, ok := a.cusByS[uid]; !ok {
                a.cusByS[uid] = []int64{}
            }
            if fixedUid == uid {
                a.cusByS[uid] = appendUniqIds(a.cusByS[uid], _cusByS[prevUid])
            }
        }
    }
    return nil
}

func (a *AssociateBySources) findStartDate(uid string) (string, error) {
    return findStartDate(a.tx, a.cusByS[uid][0])
}

func (a *AssociateBySources) findPrevCollection(uid string) (*models.Collection, error) {
    cus := a.cusByS[uid]
    c, err := models.Collections(
        models.CollectionWhere.TypeID.EQ(common.CONTENT_TYPE_REGISTRY.ByName[common.CT_LESSONS_SERIES].ID),
        qm.InnerJoin("collections_content_units ccu ON ccu.collection_id = id"),
        qm.WhereIn("ccu.content_unit_id IN ?", utils.ConvertArgsInt64(cus)...),
        qm.Where("properties->>'source' = ?", uid),
        qm.OrderBy("(properties->>'end_date')::date DESC, id DESC"),
    ).One(a.tx)
    if err != nil {
        return nil, err
    }
    return c, nil
}

func (a *AssociateBySources) createCollection(uid string) (*models.Collection, error) {
    startDate, err := a.findStartDate(uid)
    if err != nil {
        return nil, err
    }
    props := map[string]interface{}{
        "source":     uid,
        "start_date": startDate,
    }
    c, err := CreateCollection(a.tx, common.CT_LESSONS_SERIES, props)
    if err != nil {
        return nil, err
    }
    c.Published = true
    _, err = c.Update(a.tx, boil.Whitelist("published"))
    if err != nil {
        return nil, err
    }

    addCCUs := make([]*models.CollectionsContentUnit, 0)
    for i, id := range a.cusByS[uid] {
        ccu := &models.CollectionsContentUnit{
            ContentUnitID: id,
            CollectionID:  c.ID,
            Position:      i + 1,
            Name:          fmt.Sprintf("%d", int(math.Max(float64(i+1), 1))),
        }
        addCCUs = append(addCCUs, ccu)
    }
    if err := c.AddCollectionsContentUnits(a.tx, true, addCCUs...); err != nil {
        return nil, err
    }

    if err = DescribeCollection(a.tx, c); err != nil {
        return nil, err
    }

    a.evnts = append(a.evnts, events.CollectionCreateEvent(c))
    return c, nil
}

func (a *AssociateBySources) attachCollection(c *models.Collection, uid string) error {
    return attachCollection(a.tx, c, a.cu, a.cusByS[uid])
}

var qCusByL = `
  SELECT DISTINCT ON(lcu.id) lcu.uid, array_agg(DISTINCT cu.id)
  FROM content_units cu
  INNER JOIN content_unit_derivations dcu ON cu.id = dcu.source_id
  INNER JOIN content_units lcu ON lcu.id = dcu.derived_id
  WHERE cu.type_id = $1
  AND coalesce((cu.properties->>'film_date')::date, cu.created_at) > (CURRENT_DATE - '%d day'::interval)
  AND cu.published = TRUE AND cu.secure = 0
  AND lcu.uid IN (%s)
  GROUP BY  lcu.id
`

type AssociateByLikutim struct {
    tx     boil.Executor
    cu     *models.ContentUnit
    evnts  []events.Event
    cusByL map[string][]int64
    lUIDs  []string
}

func (a *AssociateByLikutim) Associate(lUIDs []string) ([]events.Event, error) {
    a.evnts = make([]events.Event, 0)
    a.cusByL = make(map[string][]int64)
    a.lUIDs = lUIDs
    if err := a.prepareCUs(); err != nil {
        return nil, NewInternalError(err)
    }

    for _, lUid := range a.lUIDs {
        if len(a.cusByL[lUid]) < MinCuNumberForNewLessonSeries {
            continue
        }
        c, err := a.findPrevCollection(lUid)
        if errors.Is(err, sql.ErrNoRows) {
            c, err = a.createCollection(lUid)
        }
        if err != nil {
            return nil, NewInternalError(err)
        }

        if err := a.attachCollection(c, lUid); err != nil {
            return nil, NewInternalError(err)
        }
    }

    return a.evnts, nil
}

func (a *AssociateByLikutim) prepareCUs() error {
    q := fmt.Sprintf(qCusByL, DaysCheckForLessonsSeries, fmt.Sprintf("'%s'", strings.Join(a.lUIDs, "','")))
    rows, err := queries.Raw(q, common.CONTENT_TYPE_REGISTRY.ByName[common.CT_LESSON_PART].ID).Query(a.tx)

    if err != nil {
        return err
    }
    defer rows.Close()

    var lUId string
    var cuIdsByL pq.Int64Array
    var cuIds []int64
    for rows.Next() {
        err = rows.Scan(&lUId, &cuIdsByL)
        if err != nil {
            return err
        }
        a.cusByL[lUId] = cuIdsByL
        cuIds = append(cuIds, cuIdsByL...)
    }

    return nil
}

func (a *AssociateByLikutim) findStartDate(uid string) (string, error) {
    return findStartDate(a.tx, a.cusByL[uid][0])
}

func (a *AssociateByLikutim) findPrevCollection(uid string) (*models.Collection, error) {
    cus := a.cusByL[uid]
    c, err := models.Collections(
        models.CollectionWhere.TypeID.EQ(common.CONTENT_TYPE_REGISTRY.ByName[common.CT_LESSONS_SERIES].ID),
        qm.InnerJoin("collections_content_units ccu ON ccu.collection_id = id"),
        qm.WhereIn("ccu.content_unit_id IN ?", utils.ConvertArgsInt64(cus)...),
        qm.Where(`properties->'likutim' @> ?`, fmt.Sprintf(`["%s"]`, uid)),
    ).One(a.tx)

    if err != nil {
        return nil, err
    }

    return c, nil
}

func (a *AssociateByLikutim) createCollection(uid string) (*models.Collection, error) {
    startDate, err := a.findStartDate(uid)
    if err != nil {
        return nil, err
    }
    props := map[string]interface{}{
        "likutim":    []string{uid},
        "start_date": startDate,
    }
    likut, err := models.ContentUnits(
        models.ContentUnitWhere.UID.EQ(uid),
        qm.Load("Tags"),
    ).One(a.tx)
    if err != nil {
        return nil, err
    }
    var tags []string
    if likut.R.Tags != nil {
        for _, t := range likut.R.Tags {
            tags = append(tags, t.UID)
        }
        props["tags"] = tags
    }
    c, err := CreateCollection(a.tx, common.CT_LESSONS_SERIES, props)
    if err != nil {
        return nil, err
    }
    c.Published = true
    _, err = c.Update(a.tx, boil.Whitelist("published"))
    if err != nil {
        return nil, err
    }

    addCCUs := make([]*models.CollectionsContentUnit, 0)
    for i, id := range a.cusByL[uid] {
        ccu := &models.CollectionsContentUnit{
            ContentUnitID: id,
            Position:      i + 1,
        }
        addCCUs = append(addCCUs, ccu)
    }
    if err := c.AddCollectionsContentUnits(a.tx, true, addCCUs...); err != nil {
        return nil, err
    }

    if err := I18nFromCU(a.tx, uid, c); err != nil {
        return nil, err
    }
    a.evnts = append(a.evnts, events.CollectionCreateEvent(c))
    return c, nil
}

func (a *AssociateByLikutim) attachCollection(c *models.Collection, uid string) error {
    return attachCollection(a.tx, c, a.cu, a.cusByL[uid])
}

// Helpers

func MapParentByLeaf(exec boil.Executor, uids []string) (map[string]string, error) {
    q := fmt.Sprintf(`
WITH RECURSIVE recurcive_s(id, uid, parent_id, start_uid) AS(
    SELECT id, uid, parent_id, uid
        FROM sources where uid IN (%s)
    UNION
    SELECT s.id, s.uid, s.parent_id, rs.start_uid
        FROM recurcive_s rs, sources s WHERE rs.parent_id = s.id AND rs.uid != '%s'
)
SELECT start_uid, uid FROM recurcive_s WHERE uid IN (%s)
`,
        fmt.Sprintf("'%s'", strings.Join(uids, "','")),
        ZOAR_PART_ONE_UID,
        fmt.Sprintf("'%s'", strings.Join(append(TES_PARTS_UIDS, ZOAR_UID, ZOAR_PART_ONE_UID), "','")),
    )
    rows, err := queries.Raw(q).Query(exec)
    if err != nil {
        return nil, NewInternalError(err)
    }
    defer rows.Close()
    var uid string
    var nUid string
    newByOldUid := make(map[string]string)
    for rows.Next() {
        err = rows.Scan(&uid, &nUid)
        if err != nil {
            return nil, NewInternalError(err)
        }
        //if we get more then one parent uid its mean that its zoar and its child ZOAR_PART_ONE_UID.
        // on this mode need take ZOAR_PART_ONE_UID
        if _, ok := newByOldUid[uid]; !ok {
            newByOldUid[uid] = nUid
        }
    }

    for _, uid := range uids {
        if _, ok := newByOldUid[uid]; !ok {
            newByOldUid[uid] = uid
        }
    }

    return newByOldUid, nil
}

func appendUniqIds(ids1, ids2 []int64) []int64 {
    for _, id2 := range ids2 {
        isIn := false
        for _, id1 := range ids1 {
            if id1 == id2 {
                isIn = true
                break
            }
        }
        if !isIn {
            ids1 = append(ids1, id2)
        }
    }
    return ids1
}

func findStartDate(tx boil.Executor, id int64) (string, error) {
    var _props map[string]interface{}
    cu, err := models.FindContentUnit(tx, id)
    if err != nil {
        return "", err
    }
    if err := cu.Properties.Unmarshal(&_props); err != nil {
        return "", err
    }
    fd, ok := _props["film_date"]
    if !ok {
        return "", NewInternalError(errors.New("no film date"))
    }
    return fd.(string), nil
}

var LAST_POSITION_BY_C_SQL = `
SELECT ccu.position
FROM collections_content_units ccu
WHERE ccu.collection_id = %d
ORDER BY ccu.position DESC
LIMIT 1
`

func attachCollection(tx boil.Executor, c *models.Collection, cu *models.ContentUnit, cus []int64) error {
    var prevPos int64

    if err := queries.Raw(fmt.Sprintf(LAST_POSITION_BY_C_SQL, c.ID)).QueryRow(tx).Scan(&prevPos); err != nil {
        return err
    }
    //position start from 0 when Name from 1
    ccu := &models.CollectionsContentUnit{
        ContentUnitID: cu.ID,
        CollectionID:  c.ID,
        Position:      int(prevPos) + 1,
        Name:          fmt.Sprintf("%d", int(math.Max(float64(prevPos+1), 1))),
    }
    if err := c.AddCollectionsContentUnits(tx, true, ccu); err != nil {
        return err
    }

    var cuProps map[string]interface{}
    if err := cu.Properties.Unmarshal(&cuProps); err != nil {
        return err
    }
    if err := UpdateCollectionProperties(tx, c, map[string]interface{}{"end_date": cuProps["film_date"]}); err != nil {
        return err
    }

    return nil
}

func I18nFromCU(tx boil.Executor, uid string, c *models.Collection) error {
    cusI18ns, err := models.ContentUnits(qm.Where("uid = ?", uid), qm.Load("ContentUnitI18ns")).One(tx)
    if err != nil && err != sql.ErrNoRows {
        return err
    }
    if cusI18ns != nil {
        i18ns := make([]*models.CollectionI18n, 0)
        for _, sI18n := range cusI18ns.R.ContentUnitI18ns {
            i18n := &models.CollectionI18n{
                Language: sI18n.Language,
                Name:     sI18n.Name,
            }
            i18ns = append(i18ns, i18n)
        }
        if err = c.AddCollectionI18ns(tx, true, i18ns...); err != nil {
            return err
        }
    }
    return nil
}