From 1d483c5d55bd978578a128be11e1f51f97b18046 Mon Sep 17 00:00:00 2001 From: dEthan3 Date: Tue, 27 Aug 2024 01:56:58 +0800 Subject: [PATCH] Initial commit --- .github/workflows/ReopBot.yml | 127 ++++++++++ README.md | 146 +++++++++++ Template.md | 69 ++++++ sync_status_readme.py | 443 ++++++++++++++++++++++++++++++++++ 4 files changed, 785 insertions(+) create mode 100644 .github/workflows/ReopBot.yml create mode 100644 README.md create mode 100644 Template.md create mode 100644 sync_status_readme.py diff --git a/.github/workflows/ReopBot.yml b/.github/workflows/ReopBot.yml new file mode 100644 index 0000000..5beea23 --- /dev/null +++ b/.github/workflows/ReopBot.yml @@ -0,0 +1,127 @@ +name: Repo Management + +on: + pull_request_target: + types: [closed] + schedule: + - cron: "0 0 * * *" # 每天午夜运行 + push: + branches: [main] # 每次推送到main分支时也运行 + +permissions: + contents: write + pull-requests: write + +jobs: + invite-contributor: + runs-on: ubuntu-latest + if: github.event_name == 'pull_request_target' && github.event.pull_request.merged == true + steps: + - name: Invite contributor + id: invite-contributor + uses: actions/github-script@v6 + with: + github-token: ${{ secrets.PAT_WITH_INVITE_PERMISSIONS }} + script: | + const { owner, repo } = context.repo; + const username = context.payload.pull_request.user.login; + + console.log(`Checking if ${username} is already a collaborator...`); + + try { + const { data: permissionLevel } = await github.rest.repos.getCollaboratorPermissionLevel({ + owner, + repo, + username, + }); + + if (permissionLevel.permission === 'admin' || permissionLevel.permission === 'write') { + console.log(`${username} is already a collaborator with sufficient permissions.`); + return; + } + + console.log(`${username} is a collaborator but needs permission update.`); + } catch (error) { + if (error.status !== 404) { + console.error(`Error checking collaborator status: ${error.message}`); + throw error; + } + console.log(`${username} is not a collaborator.`); + } + + try { + console.log(`Inviting ${username} as a collaborator...`); + const response = await github.rest.repos.addCollaborator({ + owner, + repo, + username, + permission: 'push' + }); + + if (response.status === 201) { + console.log(`Invitation sent to ${username} as a collaborator with push permission.`); + core.setOutput('invitation_sent', 'true'); + } else if (response.status === 204) { + console.log(`${username}'s permissions updated to push.`); + core.setOutput('invitation_sent', 'false'); + } + } catch (error) { + console.error(`Error inviting/updating collaborator: ${error.message}`); + core.setFailed(`Error inviting/updating collaborator: ${error.message}`); + } + + - name: Comment on PR + if: steps.invite-contributor.outputs.invitation_sent == 'true' + uses: actions/github-script@v6 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const { owner, repo } = context.repo; + const issue_number = context.payload.pull_request.number; + const username = context.payload.pull_request.user.login; + try { + await github.rest.issues.createComment({ + owner, + repo, + issue_number, + body: `Thanks for your contribution, @${username}! You've been invited as a collaborator with push permissions. Please check your email for the invitation.` + }); + console.log(`Comment posted on PR #${issue_number}`); + } catch (error) { + console.error(`Error posting comment: ${error.message}`); + core.setFailed(`Error posting comment: ${error.message}`); + } + + update-readme: + runs-on: ubuntu-latest + if: github.event_name == 'schedule' || github.event_name == 'push' + steps: + - uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: "3.x" + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install PyGithub pytz + - name: Update README + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + START_DATE: ${{ vars.START_DATE }} + END_DATE: ${{vars.END_DATE }} + FILE_SUFFIX: ${{vars.FILE_SUFFIX}} + FIELD_NAME: ${{vars.FIELD_NAME}} + run: python sync_status_readme.py + - name: Check for changes + id: git-check + run: | + git diff --exit-code README.md || echo "modified=true" >> $GITHUB_OUTPUT + - name: Commit changes + if: steps.git-check.outputs.modified == 'true' + run: | + git config --local user.email "action@github.com" + git config --local user.name "GitHub Action" + git add README.md + git commit -m "Update commit status table" + git push diff --git a/README.md b/README.md new file mode 100644 index 0000000..0bdc611 --- /dev/null +++ b/README.md @@ -0,0 +1,146 @@ +# 残酷共学模版 + +> 本文档为创建残酷共学的通用模版 - 中文版,请根据模版结构来进行你的残酷共学的内容填充 + +# {本期残酷共学标题} + +## 什么是残酷共学(Intensive Co-learning)? + +残酷共学是由 [Bruce Xu](https://twitter.com/brucexu_eth) 首创的一种学习模式,目前由 [LXDAO](https://lxdao.io/) 组织并运营残酷共学品牌。 +共学有很多种,「残酷共学」与之不同的是「残酷」: + +- 你必须每天围绕某个「共学主题」进行学习,每周只有两次请假机会,通常每天至少需要花费半个小时(最好一个小时)来学习。 +- 你必须提交你的学习证明(按照共学内容设计)到这个「仓库」来证明你今天学习了。 +- 如果你没有完成上面两点,你会立刻被踢掉并且标记为 ❌ 失败。 +- 每期残酷共学以 4 周为一个周期,第一周为共学启动报名和熟悉共学规则,第二周到第四周将正式启动共学,为期 21 天,中途不得加入。 +- 共学方向包括不限于:英语、以太坊、Web3 技术、DAO、加密思潮等,自由自主发起。共学的过程包括且不限于:观看视频、阅读书籍与文章、项目实战等。 + +报名方式是完全基于 GitHub 的流程,通过提交 PR 进行申请,合并 PR 之后拥有更新权限。如果你不熟悉 GitHub 和 Git 的操作,请先自行学习。通常还会有一个小型的 Telegram 交流群方便交流。 + +关于更多「残酷共学」的介绍请参见:https://forum.lxdao.io/t/topic/1654 + +关于更多正在发生的残酷共学请参见:https://intensivecolearn.ing/ + +如果你有任何有关残酷共学的疑问或者想法,请到 [残酷共学 Telegram 群](https://t.me/LXDAO/6215) 联系我们。 + +## {本期残酷共学名字}介绍 + +请写清楚本期残酷共学: + +- 举办的原因 +- 谁/哪个组织发起的,以及合作方 +- 一共几期 +- 共学形式:自主学习、定期答疑、线上课程 、线下 Meetup (请自由组合或新增新的共学方式) +- 本次共学目标或产出 +- 适合人群 +- 负责人、助教、导师的简单介绍以及联系方式 + +## 共学内容 + +请写清楚共学内容的链接以及使用方法,如果欢迎新增共学内容,也请说明一下,但请负责人保证共学的内容准确、质量、数量、符合本次共学难度。 + +如果有提供的具体的课程学习计划,也请在此说明。 + +## 共学时间 + +- 报名截止时间:(请写明时区) +- 本期共学开始时间:(请写明时区) +- 本期共学持续时间:21 天(我们默认为 21 天,21 天为养成一个新习惯的周期,可根据自己的内容和课程来制定,但不易过长或过短) + +## 共学规则 + +(以下内容为 LXDAO 共学活动默认规则,你可以根据自己共学的情况进行修改,请注意我们有自动化脚本进行打卡记录的更新,请确保如果修改规则要将脚本规则一并修改) + +- 报名规则:请在报名截止时间前进行报名,共学一旦开始后,不得中途加入 +- 打卡规则:建议你每天学习 30 ~ 60 分钟,并将学习笔记提交,我们会自动更新你的打卡状态,每周有两次请假的机会,超过后状态变为 ❌,视为本次共学失败 +- 激励规则:(如果有具体的激励方式请写明)(没有激励方案默认文案参考:通过本次共学学到的知识,就是你给自己最好的激励!) +- 考核规则:(如果有具体的考核方式请写明,没有就不写) + +## 如何报名和打卡? + +因为残酷共学的报名和打卡是基于 GitHub 进行开展的,如果你是非开发者或者对 git 操作不熟悉,请先阅读此文档:[残酷共学 GitHub 新手教程](https://www.notion.so/lxdao/GitHub-53fca5ba49bb40c69e4e40e69f58f416) + +- 报名: + + - Step01:Fork 本仓库。 + - Step02:复制 Template.md 创建你的个人笔记文件,并根据文档指引填写你的信息,并将文件重命名为你的名字:YourName.md。 + - Step03:创建一个 PR 到当前仓库,本残酷共学助教会对你的 PR 进行 review,review 通过后,你的 PR 会被 merge 到 main 分支,这个时候你会收到邀请加入这个仓库 contribution 的邮件,接受邀请后,你会自动获得 main 分支的 push 权限。 + - Step04:完成以上三个步骤,恭喜你报名成功,后续就可以将你的学习记录直接 push 到 main 分支进行更新。 + - 请加入 xxx 群组保持交流:(请添加你创建的群组链接)。加入群组后请在群里报到一下方便助教记录。 + +- 打卡: + - 报名成功后,你将拥有 main 分支的 push 权限,你需要将每天学习笔记按日期更新到你的 YourName.md 文档中,提交更新后,我们会自动更新你的打卡状态到下面的打卡记录表。 + - 如果你不在 UTC+8 时区,需要添加时区 code 到你的 YourName.md 文件的开始,错误的时区设置可能会使自动化打卡脚本错误计算打卡时间,具体请参考:https://github.com/IntensiveCoLearning/template/blob/main/Template.md?plain=1#L1 + - 当你提交笔记时,请确保以下几点,否则打卡可能会失败: + - 在 YourName.md 文档,请将笔记内容放到以下代码块中,且 `` 和 `` 不能删除: + ``` + + ### 日期 + 笔记内容 + + ``` + - 日期格式为 `### 2024.07.11`,请不要随意更改 + +## {本期残酷共学名字}打卡记录表 + +✅ = Done ⭕️ = Missed ❌ = Failed + + + +| Name(GitHub ID) | 6.24 | 6.25 | 6.26 | 6.27 | 6.28 | 6.29 | 6.30 | 7.01 | 7.02 | 7.03 | 7.04 | 7.05 | 7.06 | 7.07 | 7.08 | 7.09 | 7.10 | 7.11 | 7.12 | 7.13 | 7.14 | +| --------------- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | +| | | | | | | | | | | | | | | | | | | | | | | +| | | | | | | | | | | | | | | | | | | | | | | +| | | | | | | | | | | | | | | | | | | | | | | +| | | | | | | | | | | | | | | | | | | | | | | +| | | | | | | | | | | | | | | | | | | | | | | +| | | | | | | | | | | | | | | | | | | | | | | +| | | | | | | | | | | | | | | | | | | | | | | +| | | | | | | | | | | | | | | | | | | | | | | +| | | | | | | | | | | | | | | | | | | | | | | +| | | | | | | | | | | | | | | | | | | | | | | +| | | | | | | | | | | | | | | | | | | | | | | +| | | | | | | | | | | | | | | | | | | | | | | +| | | | | | | | | | | | | | | | | | | | | | | +| | | | | | | | | | | | | | | | | | | | | | | + + + + + + +> 如果你是此次共学发起人,请进行以下操作进行自动化发放权限的设置,完成后请将这一部分内容从你的仓库中删掉。 + +### 为您的组织 【残酷共学营】 创建具有邀请协作者权限的个人访问令牌 + +要创建具有邀请协作者权限的个人访问令牌,请按照以下步骤操作: + +1. 导航到您的个人设置: 转到 https://github.com/settings/profile 并登录您的 GitHub 帐户。 + +2. 访问个人访问令牌页面: 在左侧菜单中,单击 “开发者设置”,然后选择 “个人访问令牌”。 + +3. 创建新令牌: 点击 “生成新令牌” 按钮。选择 classic 的 + +4. 命名您的令牌: 在 “令牌名称” 字段中输入一个描述性名称,例如 `invite-collaborators`。 + +5. 选择适当的范围: 在 “范围” 部分,选择授予您的令牌所需的权限。对于邀请协作者,您需要授予以下范围: + +- `repo:invite`:允许您的令牌创建存储库邀请。最好是给这个令牌赋予 repo 总权限 +- `admin:org` 权限的用户才能创建具有邀请协作者权限的个人访问令牌。 + +6. 将令牌值添加到存储库 secret: 按照上述步骤将您的个人访问令牌值添加到您的存储库 secret 中,并将名称设置为 `PAT_WITH_INVITE_PERMISSIONS`。 + +![image](https://github.com/user-attachments/assets/d7c06540-9076-4557-b911-e5e484a742bb) + +### 配置共学信息配置变量 + +1. 配置这四个仓库变量(Repository variables),注意不要添加**换行**,**空格**符号 + +| Field Name | Value | Comments | +| ----------- | ------------------------- | ------------------------------- | +| START_DATE | 2024-06-24T00:00:00+00:00 | Start time | +| END_DATE | 2024-07-06T23:59:59+00:00 | End time | +| FIELD_NAME | EICL1st· Name | Field name in the readme | +| FILE_SUFFIX | _EICL1st.md_ | Shared learning activity number | + +![image](https://github.com/user-attachments/assets/d5b6f504-9eea-4215-9848-056fc33f00f8) diff --git a/Template.md b/Template.md new file mode 100644 index 0000000..934e50b --- /dev/null +++ b/Template.md @@ -0,0 +1,69 @@ +--- +timezone: Pacific/Auckland +--- + +> 请在上边的 timezone 添加你的当地时区,这会有助于你的打卡状态的自动化更新,如果没有添加,默认为北京时间 UTC+8 时区 +> 时区请参考以下列表,请移除 # 以后的内容 + +timezone: Pacific/Honolulu # 夏威夷-阿留申标准时间 (UTC-10) + +timezone: America/Anchorage # 阿拉斯加标准时间 (UTC-9) + +timezone: America/Los_Angeles # 太平洋标准时间 (UTC-8) + +timezone: America/Denver # 山地标准时间 (UTC-7) + +timezone: America/Chicago # 中部标准时间 (UTC-6) + +timezone: America/New_York # 东部标准时间 (UTC-5) + +timezone: America/Halifax # 大西洋标准时间 (UTC-4) + +timezone: America/St_Johns # 纽芬兰标准时间 (UTC-3:30) + +timezone: America/Sao_Paulo # 巴西利亚时间 (UTC-3) + +timezone: Atlantic/Azores # 亚速尔群岛时间 (UTC-1) + +timezone: Europe/London # 格林威治标准时间 (UTC+0) + +timezone: Europe/Berlin # 中欧标准时间 (UTC+1) + +timezone: Europe/Helsinki # 东欧标准时间 (UTC+2) + +timezone: Europe/Moscow # 莫斯科标准时间 (UTC+3) + +timezone: Asia/Dubai # 海湾标准时间 (UTC+4) + +timezone: Asia/Kolkata # 印度标准时间 (UTC+5:30) + +timezone: Asia/Dhaka # 孟加拉国标准时间 (UTC+6) + +timezone: Asia/Bangkok # 中南半岛时间 (UTC+7) + +timezone: Asia/Shanghai # 中国标准时间 (UTC+8) + +timezone: Asia/Tokyo # 日本标准时间 (UTC+9) + +timezone: Australia/Sydney # 澳大利亚东部标准时间 (UTC+10) + +timezone: Pacific/Auckland # 新西兰标准时间 (UTC+12) + +--- + +# {你的名字} + +1. 自我介绍 +2. 你认为你会完成本次残酷学习吗? + +## Notes + + + +### 2024.07.11 + +笔记内容 + +### 2024.07.12 + + diff --git a/sync_status_readme.py b/sync_status_readme.py new file mode 100644 index 0000000..922c122 --- /dev/null +++ b/sync_status_readme.py @@ -0,0 +1,443 @@ +import os +import subprocess +import re +import requests +from datetime import datetime, timedelta +import pytz +import logging + +# Constants +START_DATE = datetime.fromisoformat(os.environ.get( + 'START_DATE', '2024-06-24T00:00:00+00:00')).replace(tzinfo=pytz.UTC) +END_DATE = datetime.fromisoformat(os.environ.get( + 'END_DATE', '2024-07-14T23:59:59+00:00')).replace(tzinfo=pytz.UTC) +DEFAULT_TIMEZONE = 'Asia/Shanghai' +FILE_SUFFIX = os.environ.get('FILE_SUFFIX', '_EICL1st.md') +README_FILE = 'README.md' +FIELD_NAME = os.environ.get('FIELD_NAME', 'EICL1st· Name') +Content_START_MARKER = "" +Content_END_MARKER = "" +TABLE_START_MARKER = "" +TABLE_END_MARKER = "" +GITHUB_REPOSITORY_OWNER = os.environ.get('GITHUB_REPOSITORY_OWNER') +GITHUB_REPOSITORY = os.environ.get('GITHUB_REPOSITORY') + +# Configure logging +logging.basicConfig(level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s') + + +def print_env(): + print(f""" + START_DATE: {START_DATE} + END_DATE: {END_DATE} + DEFAULT_TIMEZONE: {DEFAULT_TIMEZONE} + FILE_SUFFIX: {FILE_SUFFIX} + README_FILE: {README_FILE} + FIELD_NAME: {FIELD_NAME} + Content_START_MARKER: {Content_START_MARKER} + Content_END_MARKER: {Content_END_MARKER} + TABLE_START_MARKER: {TABLE_START_MARKER} + TABLE_END_MARKER: {TABLE_END_MARKER} + """) + + +def print_variables(*args, **kwargs): + def format_value(value): + if isinstance(value, str) and ('\n' in value or '\r' in value): + return f'"""\n{value}\n"""' + return repr(value) + + variables = {} + + # 处理位置参数 + for arg in args: + if isinstance(arg, dict): + variables.update(arg) + else: + variables[arg] = eval(arg) + + # 处理关键字参数 + variables.update(kwargs) + + # 打印变量 + for name, value in variables.items(): + print(f"{name}: {format_value(value)}") + + +def get_date_range(): + return [START_DATE + timedelta(days=x) for x in range((END_DATE - START_DATE).days + 1)] + + +def get_user_timezone(file_content): + yaml_match = re.search(r'---\s*\ntimezone:\s*(\S+)\s*\n---', file_content) + if yaml_match: + try: + return pytz.timezone(yaml_match.group(1)) + except pytz.exceptions.UnknownTimeZoneError: + logging.warning( + f"Unknown timezone: {yaml_match.group(1)}. Using default {DEFAULT_TIMEZONE}.") + return pytz.timezone(DEFAULT_TIMEZONE) + + +def extract_content_between_markers(file_content): + start_index = file_content.find(Content_START_MARKER) + end_index = file_content.find(Content_END_MARKER) + if start_index == -1 or end_index == -1: + logging.warning("Content_START_MARKER markers not found in the file") + return "" + return file_content[start_index + len(Content_START_MARKER):end_index].strip() + + +def find_date_in_content(content, local_date): + date_patterns = [ + r'###\s*' + local_date.strftime("%Y.%m.%d"), + r'###\s*' + local_date.strftime("%Y.%m.%d").replace('.0', '.'), + r'###\s*' + + local_date.strftime("%m.%d").lstrip('0').replace('.0', '.'), + r'###\s*' + local_date.strftime("%Y/%m/%d"), + r'###\s*' + + local_date.strftime("%m/%d").lstrip('0').replace('/0', '/'), + r'###\s*' + local_date.strftime("%m.%d").zfill(5) + ] + combined_pattern = '|'.join(date_patterns) + return re.search(combined_pattern, content) + + +def get_content_for_date(content, start_pos): + next_date_pattern = r'###\s*(\d{4}\.)?(\d{1,2}[\.\/]\d{1,2})' + next_date_match = re.search(next_date_pattern, content[start_pos:]) + if next_date_match: + return content[start_pos:start_pos + next_date_match.start()] + return content[start_pos:] + + +def check_md_content(file_content, date, user_tz): + try: + content = extract_content_between_markers(file_content) + local_date = date.astimezone(user_tz).replace( + hour=0, minute=0, second=0, microsecond=0) + current_date_match = find_date_in_content(content, local_date) + + if not current_date_match: + logging.info( + f"No match found for date {local_date.strftime('%Y-%m-%d')}") + return False + + date_content = get_content_for_date(content, current_date_match.end()) + date_content = re.sub(r'\s', '', date_content) + logging.info( + f"Content length for {local_date.strftime('%Y-%m-%d')}: {len(date_content)}") + return len(date_content) > 10 + except Exception as e: + logging.error(f"Error in check_md_content: {str(e)}") + return False + + +def get_user_study_status(nickname): + user_status = {} + file_name = f"{nickname}{FILE_SUFFIX}" + try: + with open(file_name, 'r', encoding='utf-8') as file: + file_content = file.read() + user_tz = get_user_timezone(file_content) + logging.info( + f"File content length for {nickname}: {len(file_content)} user_tz: {user_tz}") + current_date = datetime.now(user_tz).replace( + hour=0, minute=0, second=0, microsecond=0) # - timedelta(days=1) + + for date in get_date_range(): + local_date = date.astimezone(user_tz).replace( + hour=0, minute=0, second=0, microsecond=0) + + if date.day == current_date.day: + user_status[date] = "✅" if check_md_content( + file_content, date, pytz.UTC) else " " + elif date > current_date: + user_status[date] = " " + else: + user_status[date] = "✅" if check_md_content( + file_content, date, pytz.UTC) else "⭕️" + + logging.info(f"Successfully processed file for user: {nickname}") + except FileNotFoundError: + logging.error(f"Error: Could not find file {file_name}") + user_status = {date: "⭕️" for date in get_date_range()} + except Exception as e: + logging.error( + f"Unexpected error processing file for {nickname}: {str(e)}") + user_status = {date: "⭕️" for date in get_date_range()} + return user_status + + +def check_weekly_status(user_status, date, user_tz): + try: + local_date = date.astimezone(user_tz).replace( + hour=0, minute=0, second=0, microsecond=0) + week_start = (local_date - timedelta(days=local_date.weekday())) + week_dates = [week_start + timedelta(days=x) for x in range(7)] + current_date = datetime.now(user_tz).replace( + hour=0, minute=0, second=0, microsecond=0) + week_dates = [d for d in week_dates if d.astimezone(pytz.UTC).date() in [ + date.date() for date in get_date_range()] and d <= min(local_date, current_date)] + + missing_days = sum(1 for d in week_dates if user_status.get(datetime.combine( + d.astimezone(pytz.UTC).date(), datetime.min.time()).replace(tzinfo=pytz.UTC), "⭕️") == "⭕️") + + if local_date == current_date and missing_days > 2: + return "❌" + elif local_date < current_date and missing_days > 2: + return "❌" + elif local_date > current_date: + return " " + else: + return user_status.get(datetime.combine(date.date(), datetime.min.time()).replace(tzinfo=pytz.UTC), "⭕️") + except Exception as e: + logging.error(f"Error in check_weekly_status: {str(e)}") + return "⭕️" + + +def get_all_user_files(): + return [f[:-len(FILE_SUFFIX)] for f in os.listdir('.') + if f.endswith(FILE_SUFFIX) and not f.startswith('Template')] + + +def update_readme(content): + try: + start_index = content.find(TABLE_START_MARKER) + end_index = content.find(TABLE_END_MARKER) + if start_index == -1 or end_index == -1: + logging.error( + "Error: Couldn't find the table markers in README.md") + return content + + new_table = [ + f'{TABLE_START_MARKER}\n', + f'| {FIELD_NAME} | ' + + ' | '.join(date.strftime("%m.%d").lstrip('0') + for date in get_date_range()) + ' |\n', + '| ------------- | ' + + ' | '.join(['----' for _ in get_date_range()]) + ' |\n' + ] + + existing_users = set() + table_rows = content[start_index + + len(TABLE_START_MARKER):end_index].strip().split('\n')[2:] + + for row in table_rows: + match = re.match(r'\|\s*([^|]+)\s*\|', row) + if match: + display_name = match.group(1).strip() + if display_name: # 检查 display_name 是否为非空 + existing_users.add(display_name) + new_table.append(generate_user_row(display_name)) + else: + logging.warning( + f"Skipping empty display name in row: {row}") + else: + logging.warning(f"Skipping invalid row: {row}") + + new_users = set(get_all_user_files()) - existing_users + for user in new_users: + if user.strip(): # 确保用户名不是空的或只包含空格 + new_table.append(generate_user_row(user)) + logging.info(f"Added new user: {user}") + else: + logging.warning(f"Skipping empty user: '{user}'") + new_table.append(f'{TABLE_END_MARKER}\n') + return content[:start_index] + ''.join(new_table) + content[end_index + len(TABLE_END_MARKER):] + except Exception as e: + logging.error(f"Error in update_readme: {str(e)}") + return content + + +def generate_user_row(user): + user_status = get_user_study_status(user) + with open(f"{user}{FILE_SUFFIX}", 'r', encoding='utf-8') as file: + file_content = file.read() + user_tz = get_user_timezone(file_content) + new_row = f"| {user} |" + is_eliminated = False + absent_count = 0 + current_week = None + + user_current_day = datetime.now(user_tz).replace( + hour=0, minute=0, second=0, microsecond=0) + for date in get_date_range(): + # 获取用户时区和当地时间进行比较,如果用户打卡时间大于当地时间,则不显示- timedelta(days=1) + user_datetime = date.astimezone(pytz.UTC).replace( + hour=0, minute=0, second=0, microsecond=0) + if is_eliminated or (user_datetime > user_current_day and user_datetime.day > user_current_day.day): + new_row += " |" + else: + user_date = user_datetime + # 检查是否是新的一周 + week = user_date.isocalendar()[1] # 获取ISO日历周数 + if week != current_week: + current_week = week + absent_count = 0 # 重置缺勤计数 + + status = user_status.get(user_date, "") + + if status == "⭕️": + absent_count += 1 + if absent_count > 2: + is_eliminated = True + new_row += " ❌ |" + else: + new_row += " ⭕️ |" + else: + new_row += f" {status} |" + + return new_row + '\n' + + +def get_repo_info(): + if 'GITHUB_REPOSITORY' in os.environ: + # 在GitHub Actions环境中 + full_repo = os.environ['GITHUB_REPOSITORY'] + owner, repo = full_repo.split('/') + else: + # 在本地环境中 + try: + remote_url = subprocess.check_output( + ['git', 'config', '--get', 'remote.origin.url']).decode('utf-8').strip() + if remote_url.startswith('https://github.com/'): + owner, repo = remote_url.split('/')[-2:] + elif remote_url.startswith('git@github.com:'): + owner, repo = remote_url.split(':')[-1].split('/') + else: + raise ValueError("Unsupported remote URL format") + repo = re.sub(r'\.git$', '', repo) + except subprocess.CalledProcessError: + logging.error( + "Failed to get repository information from git config") + return None, None + return owner, repo + + +def get_fork_count(): + owner, repo = get_repo_info() + if not owner or not repo: + logging.error("Failed to get repository information") + return None + + api_url = f"https://api.github.com/repos/{owner}/{repo}" + + try: + response = requests.get(api_url) + response.raise_for_status() + repo_data = response.json() + return repo_data['forks_count'] + except requests.RequestException as e: + logging.error(f"Error fetching fork count: {e}") + return None + + +def calculate_statistics(content): + start_index = content.find(TABLE_START_MARKER) + end_index = content.find(TABLE_END_MARKER) + if start_index == -1 or end_index == -1: + logging.error("Error: Couldn't find the table markers in README.md") + return None + + table_content = content[start_index + + len(TABLE_START_MARKER):end_index].strip() + rows = table_content.split('\n')[2:] # Skip header and separator rows + + total_participants = len(rows) + eliminated_participants = 0 + completed_participants = 0 + perfect_attendance_users = [] + + for row in rows: + user_name = row.split('|')[1].strip() + # Exclude first and last empty elements + statuses = [status.strip() for status in row.split('|')[2:-1]] + + if '❌' in statuses: + eliminated_participants += 1 + elif all(status == '✅' for status in statuses): + completed_participants += 1 + perfect_attendance_users.append(user_name) + elif all(status in ['✅', '⭕️', ' '] for status in statuses): + completed_participants += 1 + + elimination_rate = (eliminated_participants / + total_participants) * 100 if total_participants > 0 else 0 + fork_count = get_fork_count() + + return { + 'total_participants': total_participants, + 'completed_participants': completed_participants, + 'eliminated_participants': eliminated_participants, + 'elimination_rate': elimination_rate, + 'fork_count': fork_count, + 'perfect_attendance_users': perfect_attendance_users + } + + +def main(): + try: + print_variables( + 'START_DATE', 'END_DATE', 'DEFAULT_TIMEZONE', + GITHUB_REPOSITORY_OWNER=GITHUB_REPOSITORY, + GITHUB_REPOSITORY=GITHUB_REPOSITORY, + FILE_SUFFIX=FILE_SUFFIX, + README_FILE=README_FILE, + FIELD_NAME=FIELD_NAME, + Content_START_MARKER=Content_START_MARKER, + Content_END_MARKER=Content_END_MARKER, + TABLE_START_MARKER=TABLE_START_MARKER, + TABLE_END_MARKER=TABLE_END_MARKER + ) + with open(README_FILE, 'r', encoding='utf-8') as file: + content = file.read() + new_content = update_readme(content) + current_date = datetime.now(pytz.UTC) + if current_date > END_DATE: + stats = calculate_statistics(new_content) + if stats: + stats_content = f"\n\n## 统计数据\n\n" + stats_content += f"- 总参与人数: {stats['total_participants']}\n" + stats_content += f"- 完成人数: {stats['completed_participants']}\n" + stats_content += f"- 全勤用户: {', '.join(stats['perfect_attendance_users'])}\n" + stats_content += f"- 淘汰人数: {stats['eliminated_participants']}\n" + stats_content += f"- 淘汰率: {stats['elimination_rate']:.2f}%\n" + stats_content += f"- Fork人数: {stats['fork_count']}\n" + # 将统计数据添加到文件末尾 + # 在标记后插入统计数据 + stats_start = new_content.find( + "") + stats_end = new_content.find("") + + if stats_start != -1 and stats_end != -1: + # Replace existing statistical data + new_content = new_content[:stats_start] + "\n" + stats_content + \ + "" + \ + new_content[stats_end + + len(""):] + else: + # Add new statistical data after + end_table_marker = "" + end_table_index = new_content.find(end_table_marker) + if end_table_index != -1: + insert_position = end_table_index + \ + len(end_table_marker) + new_content = new_content[:insert_position] + "\n\n\n" + \ + stats_content + "" + \ + new_content[insert_position:] + else: + logging.warning( + " marker not found. Appending stats to the end.") + new_content += "\n\n\n" + \ + stats_content + "" + with open(README_FILE, 'w', encoding='utf-8') as file: + file.write(new_content) + logging.info("README.md has been successfully updated.") + except Exception as e: + logging.error(f"An error occurred in main function: {str(e)}") + + +if __name__ == "__main__": + main()