Skip to content
Snippets Groups Projects
Commit 839efab6 authored by Simon Künzel's avatar Simon Künzel
Browse files

Add GET /stats/course/{course_id}

parent b4cda492
Branches
Tags v2.0.23
No related merge requests found
Pipeline #7718 passed
Pipeline: backend

#7719

    ......@@ -2299,6 +2299,128 @@
    }
    ],
    "url_parameters": null
    },
    {
    "allow_while_disabled": false,
    "allow_while_readonly": true,
    "description": "",
    "only_mod": true,
    "request_objects": {},
    "requires_csrf_token": false,
    "response_description": "Note that views only appear some time after they viewer has watched the video (usually a few hours)",
    "response_object_id": "",
    "response_objects": {
    "": {
    "fields": {
    "": {
    "average_watch_speed": {
    "config_directly_modifiable": false,
    "id": "average_watch_speed",
    "notes": "",
    "object_variant": null,
    "only_mod": false,
    "optional": false,
    "type": "float"
    },
    "daily_views": {
    "config_directly_modifiable": false,
    "id": "daily_views",
    "notes": "",
    "object_variant": null,
    "only_mod": false,
    "optional": false,
    "type": "daily_views"
    },
    "lecture_views": {
    "config_directly_modifiable": false,
    "id": "lecture_views",
    "notes": "",
    "object_variant": null,
    "only_mod": false,
    "optional": false,
    "type": "lecture_views"
    },
    "total_watched_seconds": {
    "config_directly_modifiable": false,
    "id": "total_watched_seconds",
    "notes": "",
    "object_variant": null,
    "only_mod": false,
    "optional": false,
    "type": "int"
    },
    "view_count": {
    "config_directly_modifiable": false,
    "id": "view_count",
    "notes": "",
    "object_variant": null,
    "only_mod": false,
    "optional": false,
    "type": "int"
    }
    }
    },
    "id": ""
    },
    "daily_views": {
    "fields": {
    "": {
    "dates": {
    "config_directly_modifiable": false,
    "id": "dates",
    "notes": "The dates in order without gaps",
    "object_variant": null,
    "only_mod": false,
    "optional": false,
    "type": "string[]"
    },
    "view_counts": {
    "config_directly_modifiable": false,
    "id": "view_counts",
    "notes": "The count of views on the date which is at the same position in the 'dates' array",
    "object_variant": null,
    "only_mod": false,
    "optional": false,
    "type": "int[]"
    }
    }
    },
    "id": "daily_views"
    },
    "lecture_views": {
    "fields": {
    "": {
    "lecture_ids": {
    "config_directly_modifiable": false,
    "id": "lecture_ids",
    "notes": "The lecture ids (in ascending time order)",
    "object_variant": null,
    "only_mod": false,
    "optional": false,
    "type": "int[]"
    },
    "view_counts": {
    "config_directly_modifiable": false,
    "id": "view_counts",
    "notes": "The count of views for the lecture on the date which is at the same position in the 'dates' array",
    "object_variant": null,
    "only_mod": false,
    "optional": false,
    "type": "int[]"
    }
    }
    },
    "id": "lecture_views"
    }
    },
    "routes": [
    {
    "display_path": "/stats/course/{course_id}",
    "method": "GET",
    "path": "/stats/course/<int:course_id>"
    }
    ],
    "url_parameters": null
    }
    ],
    "global_objects": {
    ......
    # Specification of the Web API for the Video-AG Website (v0.86).
    # Specification of the Web API for the Video-AG Website (v0.87).
    ## Introduction
    ......@@ -1743,6 +1743,98 @@ daily_views:
    Note that views only appear some time after they viewer has watched the video (usually a few hours)
    ---
    #### `GET /stats/course/{course_id}`
    This may be used while the site is [read-only](#site-status).
    This may only be used by a moderator.
    ###### Response:
    <table>
    <thead>
    <th>Field</th>
    <th>Type</th>
    <th>Notes</th>
    </thead>
    <tbody>
    <tr>
    <td>average_watch_speed</td>
    <td>float</td>
    <td></td>
    </tr>
    <tr>
    <td>daily_views</td>
    <td>daily_views</td>
    <td></td>
    </tr>
    <tr>
    <td>lecture_views</td>
    <td>lecture_views</td>
    <td></td>
    </tr>
    <tr>
    <td>total_watched_seconds</td>
    <td>int</td>
    <td></td>
    </tr>
    <tr>
    <td>view_count</td>
    <td>int</td>
    <td></td>
    </tr>
    </tbody>
    </table>
    daily_views:
    <table>
    <thead>
    <th>Field</th>
    <th>Type</th>
    <th>Notes</th>
    </thead>
    <tbody>
    <tr>
    <td>dates</td>
    <td>string[]</td>
    <td>The dates in order without gaps</td>
    </tr>
    <tr>
    <td>view_counts</td>
    <td>int[]</td>
    <td>The count of views on the date which is at the same position in the 'dates' array</td>
    </tr>
    </tbody>
    </table>
    lecture_views:
    <table>
    <thead>
    <th>Field</th>
    <th>Type</th>
    <th>Notes</th>
    </thead>
    <tbody>
    <tr>
    <td>lecture_ids</td>
    <td>int[]</td>
    <td>The lecture ids (in ascending time order)</td>
    </tr>
    <tr>
    <td>view_counts</td>
    <td>int[]</td>
    <td>The count of views for the lecture on the date which is at the same position in the 'dates' array</td>
    </tr>
    </tbody>
    </table>
    Note that views only appear some time after they viewer has watched the video (usually a few hours)
    ---
    ......@@ -3228,6 +3320,10 @@ Possible `error_code`:
    ## Changelog
    ### v0.87
    * Added `GET /stats/course/{course_id}`
    ### v0.86
    * Updated `ffmpeg_filter_graph_node`
    ......
    # Specification of the Web API for the Video-AG Website (v0.86).
    # Specification of the Web API for the Video-AG Website (v0.87).
    ## Introduction
    ......@@ -139,6 +139,10 @@ Possible `error_code`:
    ## Changelog
    ### v0.87
    * Added `GET /stats/course/{course_id}`
    ### v0.86
    * Updated `ffmpeg_filter_graph_node`
    ......
    import math
    import re
    from datetime import timedelta
    from datetime import timedelta, date
    import more_itertools
    from typing import Sequence
    ......@@ -288,8 +288,119 @@ def api_route_get_lecture_stats(lecture_id: int):
    "total_watched_seconds": generic_lecture_stats.total_watched_seconds if generic_lecture_stats else 0,
    "average_watch_speed": generic_lecture_stats.average_watch_speed if generic_lecture_stats else 1,
    "daily_views": {
    "dates": [daily_views_dates_json],
    "view_counts": [daily_views_counts_json]
    "dates": daily_views_dates_json,
    "view_counts": daily_views_counts_json
    }
    }
    @api_route("/stats/course/<int:course_id>", "GET", allow_while_readonly=True,
    response_description="Note that views only appear some time after they viewer has watched the video (usually"
    " a few hours)",
    response_objects={
    "": [
    ("view_count", "int"),
    ("total_watched_seconds", "int"),
    ("average_watch_speed", "float"),
    ("lecture_views", "lecture_views"),
    ("daily_views", "daily_views"),
    ],
    "lecture_views": [
    ("lecture_ids", "int[]", "The lecture ids (in ascending time order)"),
    ("view_counts", "int[]", "The count of views for the lecture on the date which is at the same position in the"
    " 'dates' array")
    ],
    "daily_views": [
    ("dates", "string[]", "The dates in order without gaps"),
    ("view_counts", "int[]", "The count of views on the date which is at the same position in the"
    " 'dates' array")
    ]
    })
    @api_moderator_route()
    def api_route_get_course_stats(course_id: int):
    def _trans(session: SessionDb) -> tuple[
    Course,
    Sequence[tuple[date, int]],
    Sequence[tuple[int, int]],
    tuple[int, int, float],
    ]:
    course = session.scalar(
    Course.select({
    AC_IS_MOD: True
    }, [])
    .where(Lecture.id == course_id)
    )
    if course is None:
    raise ApiClientException(ERROR_UNKNOWN_OBJECT)
    daily_stats = session.execute(
    sql.select(LectureDailyWatchStats.date, sql.func.sum(LectureDailyWatchStats.view_count))
    .join(Lecture, LectureDailyWatchStats.lecture_id == Lecture.id)
    .where(Lecture.course_id == course_id)
    .group_by(LectureDailyWatchStats.date)
    .order_by(LectureDailyWatchStats.date.asc())
    ).all()
    lectures_view_count = session.execute(
    sql.select(LectureWatchStats.view_count, LectureWatchStats.lecture_id)
    .join(Lecture, LectureWatchStats.lecture_id == Lecture.id)
    .where(Lecture.course_id == course_id)
    .order_by(Lecture.time.asc(), Lecture.id.asc())
    ).all()
    generic_course_stats = session.execute(
    sql.select(
    sql.func.sum(LectureWatchStats.view_count),
    sql.func.sum(LectureWatchStats.total_watched_seconds),
    sql.case(
    (
    sql.func.sum(LectureWatchStats.view_count) > 0,
    sql.func.sum(LectureWatchStats.average_watch_speed * LectureWatchStats.view_count)
    / sql.func.sum(LectureWatchStats.view_count)
    ),
    else_=0
    )
    )
    .join(Lecture, LectureWatchStats.lecture_id == Lecture.id)
    .where(Lecture.course_id == course_id)
    ).one()
    session.expunge_all()
    return course, daily_stats, lectures_view_count, generic_course_stats
    course, daily_stats, lectures_view_count, generic_course_stats = database.execute_read_transaction(_trans)
    daily_views_counts_json = []
    daily_views_dates_json = []
    if len(daily_stats) > 0:
    current_date = daily_stats[0][0]
    daily_stat_iter = more_itertools.peekable(daily_stats)
    while daily_stat_iter.peek(None) is not None: # Has next
    assert daily_stat_iter.peek()[0] >= current_date
    if daily_stat_iter.peek()[0] == current_date:
    view_count = next(daily_stat_iter)[1]
    else:
    view_count = 0
    daily_views_counts_json.append(view_count)
    daily_views_dates_json.append(current_date.strftime("%d.%m.%Y"))
    current_date += timedelta(days=1)
    lecture_views_ids_json = []
    lecture_views_counts_json = []
    for row in lectures_view_count:
    lecture_views_ids_json.append(row[1])
    lecture_views_counts_json.append(row[0])
    return {
    "view_count": int(generic_course_stats[0]),
    "total_watched_seconds": int(generic_course_stats[1]),
    "average_watch_speed": float(generic_course_stats[2]),
    "lecture_views": {
    "lecture_ids": lecture_views_ids_json,
    "view_counts": lecture_views_counts_json
    },
    "daily_views": {
    "dates": daily_views_dates_json,
    "view_counts": daily_views_counts_json
    }
    }
    ......
    ......@@ -76,6 +76,7 @@ class PublishMediumWatchStats(Base):
    # Counts the views per day for a lecture
    class LectureDailyWatchStats(Base):
    lecture_id: Mapped[int] = mapped_column(ForeignKey("lecture.id"), nullable=False, primary_key=True)
    # TODO remove timezone
    date: Mapped[Date] = mapped_column(nullable=False, primary_key=True)
    view_count: Mapped[int] = mapped_column(nullable=False, default=0)
    ......
    0% Loading or .
    You are about to add 0 people to the discussion. Proceed with caution.
    Please register or to comment