diff --git a/pygameweb/__init__.py b/pygameweb/__init__.py index e7cd19c..dcb9964 100644 --- a/pygameweb/__init__.py +++ b/pygameweb/__init__.py @@ -1,4 +1,4 @@ -__version__ = '0.0.5.dev' +__version__ = '0.0.6' # So we can use environment variables to configure things. diff --git a/pygameweb/alembic/versions/e22b4355e6fd_youtube_github_and_patreon_fields_added_.py b/pygameweb/alembic/versions/e22b4355e6fd_youtube_github_and_patreon_fields_added_.py new file mode 100644 index 0000000..73ed891 --- /dev/null +++ b/pygameweb/alembic/versions/e22b4355e6fd_youtube_github_and_patreon_fields_added_.py @@ -0,0 +1,34 @@ +"""Youtube, github, and patreon fields added to project. + +Revision ID: e22b4355e6fd +Revises: 7bb2943bbcaf +Create Date: 2019-01-27 09:18:12.312081 + +""" + +# revision identifiers, used by Alembic. +revision = 'e22b4355e6fd' +down_revision = '7bb2943bbcaf' +branch_labels = None +depends_on = None + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('project', sa.Column('github_repo', sa.Text(), nullable=True)) + op.add_column('project', sa.Column('patreon', sa.Text(), nullable=True)) + op.add_column('project', sa.Column('youtube_trailer', sa.Text(), nullable=True)) + op.add_column('release', sa.Column('from_external', sa.String(length=255), nullable=True)) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('release', 'from_external') + op.drop_column('project', 'youtube_trailer') + op.drop_column('project', 'patreon') + op.drop_column('project', 'github_repo') + # ### end Alembic commands ### diff --git a/pygameweb/config.py b/pygameweb/config.py index db5b28c..bc00dd4 100644 --- a/pygameweb/config.py +++ b/pygameweb/config.py @@ -76,3 +76,8 @@ class Config(object): COMMENT_MODEL = os.getenv(CONFIG_PREFIX + 'COMMENT_MODEL', 'comment_spam_model.pkl') """For the comment spam classifier model file. """ + + GITHUB_RELEASES_OAUTH = os.getenv(CONFIG_PREFIX + 'GITHUB_RELEASES_OAUTH', None) + """ For syncing github releases to pygame_org. + """ + diff --git a/pygameweb/project/forms.py b/pygameweb/project/forms.py index 4938f2f..b61ad30 100644 --- a/pygameweb/project/forms.py +++ b/pygameweb/project/forms.py @@ -3,7 +3,7 @@ from wtforms.fields import StringField, HiddenField from wtforms.fields.html5 import URLField -from wtforms.validators import DataRequired, Required +from wtforms.validators import DataRequired, Required, URL, Optional from wtforms.widgets import TextArea @@ -12,20 +12,23 @@ class ProjectForm(FlaskForm): tags = StringField('Tags') summary = StringField('Summary', widget=TextArea(), validators=[Required()]) description = StringField('Description', widget=TextArea()) - uri = URLField('Home URL', validators=[Required()]) + uri = URLField('Home URL', validators=[Required(), URL()]) image = FileField('image', validators=[ # FileRequired(), FileAllowed(['jpg', 'png'], 'Images only!') ]) + github_repo = URLField('Github repository URL', validators=[Optional(), URL()]) + youtube_trailer = URLField('Youtube trailer URL', validators=[Optional(), URL()]) + patreon = URLField('Patreon URL', validators=[Optional(), URL()]) class ReleaseForm(FlaskForm): version = StringField('version', validators=[Required()]) description = StringField('description', widget=TextArea()) - srcuri = URLField('Source URL') - winuri = URLField('Windows URL') - macuri = URLField('Mac URL') + srcuri = URLField('Source URL', validators=[Optional(), URL()]) + winuri = URLField('Windows URL', validators=[Optional(), URL()]) + macuri = URLField('Mac URL', validators=[Optional(), URL()]) class FirstReleaseForm(ProjectForm, ReleaseForm): diff --git a/pygameweb/project/gh_releases.py b/pygameweb/project/gh_releases.py new file mode 100644 index 0000000..651d1ce --- /dev/null +++ b/pygameweb/project/gh_releases.py @@ -0,0 +1,209 @@ +""" For syncing github releases to pygame releases. +""" +import urllib.parse +import feedparser +import requests +import dateutil.parser + +from pygameweb.project.models import Project, Release +from pygameweb.config import Config + +def sync_github_releases(): + """ to the pygame website releases. + """ + from pygameweb.config import Config + from sqlalchemy import create_engine + from sqlalchemy.orm import sessionmaker + engine = create_engine(Config.SQLALCHEMY_DATABASE_URI) + + a_connection = engine.connect() + a_transaction = a_connection.begin() + session = sessionmaker(bind=a_connection)() + + projects = (session + .query(Project) + .filter(Project.github_repo.isnot(None)) + ) + for project in projects: + sync_project(session, project) + + session.commit() + a_transaction.commit() + + +def sync_project(session, project): + if not project.github_repo: + return + if project.user is not None and project.user.disabled: + return + gh_releases = get_gh_releases_feed(project) + releases = project.releases + + gh_add, gh_update, pg_delete = releases_to_sync(gh_releases, releases) + + # only do the API call once if we need to add/update. + releases_gh_api = ( + get_gh_releases_api(project) + if gh_add or gh_update else None + ) + + releases_added = [] + for gh_release in gh_add: + gh_release_api = [ + r for r in releases_gh_api + if r['name'] == gh_release['title'] + ] + if not gh_release_api or gh_release_api[0]['draft']: + continue + + release = release_from_gh(session, project, gh_release, gh_release_api[0]) + releases_added.append(release) + + for release in releases_added: + session.add(release) + + for gh_release in gh_update: + releases = [ + r for r in project.releases + if r.version == gh_release['title'] + ] + if releases: + release = releases[0] + + release.version = gh_release['title'] + release.description = gh_release['body'] + session.add(release) + + for pg_release in pg_delete: + pg_release.delete() + session.add(pg_release) + + +def release_from_gh(session, project, gh_release_atom, gh_release_api): + """ make a Release from a gh release. + + :param gh_release_atom: from the atom feed. + :param gh_release_api: from the API. + """ + winuri = '' + srcuri = '' + macuri = '' + for asset in gh_release_api['assets']: + if asset["browser_download_url"].endswith('msi'): + winuri = asset["browser_download_url"] + elif asset["browser_download_url"].endswith('tar.gz'): + srcuri = asset["browser_download_url"] + elif asset["browser_download_url"].endswith('dmg'): + macuri = asset["browser_download_url"] + + published_at = dateutil.parser.parse(gh_release_api["published_at"]) + # "2019-01-06T15:29:18Z", + + release = Release( + datetimeon=published_at, + description=gh_release_atom['content'][0]["value"], + srcuri=srcuri, + winuri=winuri, + macuri=macuri, + version=gh_release_atom['title'], + project=project + ) + return release + + +def releases_to_sync(gh_releases, releases): + """ + :param gh_releases: github release objects from atom. + :param releases: the db releases. + """ + add, update, delete = versions_to_sync(gh_releases, releases) + + gh_add = [r for r in gh_releases if r.title in add] + gh_update = [r for r in gh_releases if r.title in update] + pg_delete = [r for r in releases if r.version in delete] + return gh_add, gh_update, pg_delete + +def versions_to_sync(gh_releases, releases): + """ + :param gh_releases: github release objects from atom. + :param releases: the db releases. + """ + # Because many projects might have existing ones on pygame, + # but not have them on github, we don't delete ones unless + # they came originally from github. + return what_versions_sync( + {r.version for r in releases}, + {r.title for r in gh_releases}, + {r.version for r in releases if r.from_external == 'github'} + ) + +def what_versions_sync(pg_versions, gh_versions, pg_versions_gh): + """ versions to add, update, delete. + """ + to_add = gh_versions - pg_versions + to_update = pg_versions_gh & gh_versions + to_delete = pg_versions_gh - gh_versions + return to_add, to_update, to_delete + +def get_gh_releases_feed(project): + """ for a project. + """ + repo = project.github_repo + if not repo.endswith('/'): + repo += '/' + feed_url = urllib.parse.urljoin( + repo, + "releases.atom" + ) + data = feedparser.parse(feed_url) + if not data['feed']['title'].startswith('Release notes from'): + raise ValueError('does not appear to be a github release feed.') + return data.entries + + +def get_repo_from_url(url): + """ get the github repo from the url + """ + if not url.startswith('https://github.com/'): + return + repo = ( + urllib.parse.urlparse(url).path + .lstrip('/') + .rstrip('/') + ) + if len(repo.split('/')) != 2: + return + return repo + + +def get_gh_releases_api(project, version=None): + """ + """ + # https://developer.github.com/v3/auth/ + # self.headers = {'Authorization': 'token %s' % self.api_token} + # https://api.github.com/repos/pygame/stuntcat/releases/latest + repo = get_repo_from_url(project.github_repo) + if not repo: + return + + url = f'https://api.github.com/repos/{repo}/releases' + if version is not None: + url += f'/{version}' + + if Config.GITHUB_RELEASES_OAUTH is None: + headers = {} + else: + headers = {'Authorization': 'token %s' % Config.GITHUB_RELEASES_OAUTH} + resp = requests.get( + url, + headers = headers + ) + if resp.status_code != 200: + raise ValueError('github api failed') + + data = resp.json() + return data + + + + diff --git a/pygameweb/project/models.py b/pygameweb/project/models.py index 3660b4e..23540c7 100644 --- a/pygameweb/project/models.py +++ b/pygameweb/project/models.py @@ -3,10 +3,11 @@ from math import sqrt from pathlib import Path from email.utils import formatdate +from urllib.parse import urlparse, parse_qs, urlencode from sqlalchemy import (Column, DateTime, ForeignKey, Integer, - String, Text, inspect, func, and_) -from sqlalchemy.orm import relationship + String, Text, inspect, func, and_, or_, CheckConstraint) +from sqlalchemy.orm import relationship, validates from sqlalchemy.sql.functions import count from pyquery import PyQuery as pq @@ -34,6 +35,44 @@ class Project(Base): datetimeon = Column(DateTime) image = Column(String(80)) + github_repo = Column(Text) + """ URL to the github repo for this project. + """ + _github_repo_constraint = CheckConstraint( + or_( + github_repo is None, + github_repo == '', + github_repo.startswith('https://github.com/') + ), + name="project_github_repo_constraint" + ) + + + youtube_trailer = Column(Text) + """ URL to the youtube trailer for this project. + """ + _youtube_trailer_constraint = CheckConstraint( + or_( + youtube_trailer is None, + youtube_trailer == '', + youtube_trailer.startswith('https://www.youtube.com/watch?v=') + ), + name="project_youtube_trailer_constraint" + ) + + patreon = Column(Text) + """ URL to the patreon. + """ + _patreon_constraint = CheckConstraint( + or_( + patreon is None, + patreon == '', + patreon.startswith('https://www.patreon.com/') + ), + name="project_patreon_constraint" + ) + + def __repr__(self): return "" % self.title @@ -75,6 +114,22 @@ def tag_counts(self): return [(tag, cnt, (int(10 + min(24, sqrt(cnt) * 24 / 5)))) for tag, cnt in tag_counts] + @property + def youtube_trailer_embed(self): + if not self.youtube_trailer: + return + video_key = parse_qs(urlparse(self.youtube_trailer).query).get('v')[0] + bad_chars = ['?', ';', '&', '..', '/'] + if any(bad in video_key for bad in bad_chars): + raise ValueError('problem') + return f'http://www.youtube.com/embed/{video_key}' + + __table_args__ = ( + _github_repo_constraint, + _youtube_trailer_constraint, + _patreon_constraint, + ) + def top_tags(session, limit=30): """ @@ -156,6 +211,13 @@ class Release(Base): macuri = Column(String(255)) version = Column(String(80)) + from_external = Column(String(255)) + """ is this release sucked in from an external source. + + If it is 'github' then it comes from a github release. + If it is None, then it is user entered. + """ + project = relationship(Project, backref='releases') @property diff --git a/pygameweb/project/views.py b/pygameweb/project/views.py index 621c8d3..8cc7115 100644 --- a/pygameweb/project/views.py +++ b/pygameweb/project/views.py @@ -308,7 +308,10 @@ def new_project(): description=form.description.data, uri=form.uri.data, datetimeon=now, - user=user + user=user, + youtube_trailer=form.youtube_trailer.data, + github_repo=form.github_repo.data, + patreon=form.patreon.data, ) tags = [t.lstrip().rstrip() for t in form.tags.data.split(',')] @@ -370,6 +373,9 @@ def edit_project(project_id): project.description = form.description.data project.uri = form.uri.data project.datetimeon = datetime.datetime.now() + project.youtube_trailer = form.youtube_trailer.data + project.github_repo = form.github_repo.data + project.patreon = form.patreon.data for tag in (current_session .query(Tags) diff --git a/pygameweb/templates/project/editproject.html b/pygameweb/templates/project/editproject.html index 7b7b195..dfdf4e4 100644 --- a/pygameweb/templates/project/editproject.html +++ b/pygameweb/templates/project/editproject.html @@ -20,6 +20,9 @@

Edit Project

{{ wtf.form_field(form.summary, rows=3) }} {{ wtf.form_field(form.description, rows=5) }} {{ wtf.form_field(form.uri) }} + {{ wtf.form_field(form.github_repo) }} + {{ wtf.form_field(form.youtube_trailer) }} + {{ wtf.form_field(form.patreon) }} diff --git a/pygameweb/templates/project/view.html b/pygameweb/templates/project/view.html index 5fa8655..7ac9520 100644 --- a/pygameweb/templates/project/view.html +++ b/pygameweb/templates/project/view.html @@ -82,6 +82,27 @@

Changes

{% for value, count, size in project.tag_counts %} {{value}} {{ count }} {% endfor %} + {% if project.youtube_trailer %} + {# Make the youtube embed responsive. #} + +
+ +
+ {% endif %} @@ -104,6 +125,14 @@

Links

Mac
{{ release.macuri }}
{% endif %} + {% if project.github_repo %} +
Github repo
+
{{ project.github_repo }}
+ {% endif %} + {% if project.patreon %} +
Support this project on Patreon
+
{{ project.patreon }}
+ {% endif %} {% endif %} diff --git a/setup.py b/setup.py index aee6124..ee96f4f 100644 --- a/setup.py +++ b/setup.py @@ -80,6 +80,8 @@ def get_requirements(): 'pygameweb.tasks.worker:work', 'pygameweb_release_version_correct=' 'pygameweb.builds.update_version_from_git:release_version_correct', + 'pygameweb_github_releases=' + 'pygameweb.project.gh_releases:sync_github_releases', ], }, ) diff --git a/tests/functional/pygameweb/project/test_gh_releases.py b/tests/functional/pygameweb/project/test_gh_releases.py new file mode 100644 index 0000000..07203de --- /dev/null +++ b/tests/functional/pygameweb/project/test_gh_releases.py @@ -0,0 +1,382 @@ +import pytest + + +class Release: + def __init__(self, version, description, from_external): + self.version = version + self.description = description + self.from_external = from_external + + +class GRelease: + def __init__(self, title, content): + self.title = title + self.content = [{"value": content}] + self.draft = False + + +def make_gh(releases): + return [GRelease(r[0], r[1]) for r in releases] + + +def make_pg(releases): + return [Release(r[0], r[1], r[2]) for r in releases] + + +def test_releases_to_sync(): + """ adds the one that is missing. + """ + from pygameweb.project.gh_releases import releases_to_sync + + pg_releases = make_pg([("0.1", "r1", None), ("0.2", "r2", None)]) + gh_releases = make_gh([("0.1", "r1"), ("0.2", "r2"), ("0.3", "r3")]) + gh_add, gh_update, pg_delete = releases_to_sync(gh_releases, pg_releases) + + assert len(gh_add) == 1 + assert not gh_update and not pg_delete + assert gh_add[0].title == "0.3" + + +def test_releases_to_sync_delete(): + """ deletes the github updated one that is there. + """ + from pygameweb.project.gh_releases import releases_to_sync + + pg_releases = make_pg([("0.1", "r1", None), ("0.2", "r2", "github")]) + gh_releases = make_gh([("0.1", "r1")]) + gh_add, gh_update, pg_delete = releases_to_sync(gh_releases, pg_releases) + + assert not gh_add and not gh_update + assert len(pg_delete) == 1 + assert pg_delete[0].version == "0.2" + + +def test_releases_to_sync_update(): + """ updates the github one that changed. + """ + from pygameweb.project.gh_releases import releases_to_sync + + pg_releases = make_pg([("0.1", "r1", None), ("0.2", "r2", "github")]) + gh_releases = make_gh([("0.1", "r1"), ("0.2", "r2 with updated content")]) + gh_add, gh_update, pg_delete = releases_to_sync(gh_releases, pg_releases) + + assert len(gh_update) == 1 + assert not gh_add and not pg_delete + assert gh_update[0].title == "0.2" + assert gh_update[0].content[0]["value"] == "r2 with updated content" + + +#TODO: test a draft release. +#TODO: test sync_project deletes and updates releases properly. +#TODO: mock out real gh request. + + +def test_get_repo_from_url(): + """ gets the github repo from the url. + """ + from pygameweb.project.gh_releases import get_repo_from_url + + url = "https://github.com/pygame/pygame/" + assert get_repo_from_url(url) == "pygame/pygame" + + url = "https://github.com/pygame/pygame" + assert get_repo_from_url(url) == "pygame/pygame" + + url = "xxx" + assert get_repo_from_url(url) == None + + url = "https://github.com/pygame" + assert get_repo_from_url(url) == None + + +@pytest.fixture +def project(session): + """ links up a Project with releases, tags, and comments for testing. + """ + import datetime + from pygameweb.project.models import Project, Release, Projectcomment, Tags + + from pygameweb.user.models import User, Group + + user = User(name="name", email="email", password="", disabled=0, active=True) + + the_project = Project( + title="Stuntcat", + summary="Summary of some project 1.", + description="Description of some project.", + uri="http://some.example.com/", + datetimeon=datetime.datetime(2017, 1, 5), + image="1.png", + github_repo="https://github.com/pygame/stuntcat", + user=user, + ) + + tag1 = Tags(project=the_project, value="game") + tag2 = Tags(project=the_project, value="arcade") + session.add(tag1) + session.add(tag2) + + release1 = Release( + datetimeon=datetime.datetime(2017, 1, 5), + description="Some release.", + srcuri="http://example.com/source.tar.gz", + winuri="http://example.com/win.exe", + macuri="http://example.com/mac.dmg", + version="A release title.", + ) + + release2 = Release( + datetimeon=datetime.datetime(2017, 1, 6), + description="Some release with new things.", + srcuri="http://example.com/source.tar.gz", + winuri="http://example.com/win.exe", + macuri="http://example.com/mac.dmg", + version="A second release title.", + ) + + the_project.releases.append(release1) + the_project.releases.append(release2) + + comment1 = Projectcomment(user=user, content="Some comment 1.", rating=5) + comment2 = Projectcomment(user=user, content="Some comment 2.", rating=3) + the_project.comments.append(comment1) + the_project.comments.append(comment2) + + session.add(the_project) + session.commit() + return the_project + + +@pytest.fixture +def gh_release_atom(): + import time + return { + "author": "illume", + "author_detail": {"name": "illume"}, + "authors": [{"name": "illume"}], + "content": [ + { + "base": "https://github.com/pygame/stuntcat/releases.atom", + "language": "en-US", + "type": "text/html", + "value": "

This release can install via pip on a lot more " + "machines. Because pymunk binaries are now available " + "for mac and linux.
\n" + '

', + } + ], + "guidislink": True, + "href": "", + "id": "tag:github.com,2008:Repository/159383958/0.0.13", + "link": "https://github.com/pygame/stuntcat/releases/tag/0.0.13", + "links": [ + { + "href": "https://github.com/pygame/stuntcat/releases/tag/0.0.13", + "rel": "alternate", + "type": "text/html", + } + ], + "media_thumbnail": [ + { + "height": "30", + "url": "https://avatars3.githubusercontent.com/u/9541?s=60&v=4", + "width": "30", + } + ], + "summary": "

This release can install via pip on a lot more machines. " + "Because pymunk binaries are now available for mac and linux.
\n" + '

', + "title": "0.0.13", + "title_detail": { + "base": "https://github.com/pygame/stuntcat/releases.atom", + "language": "en-US", + "type": "text/plain", + "value": "0.0.13", + }, + "updated": "2019-01-07T09:20:56Z", + "updated_parsed": time.struct_time(( + 2019, + 1, + 7, + 9, + 20, + 56, + 0, + 7, + 0, + )), + } + + +@pytest.fixture +def gh_release_api(): + return { + "assets": [ + { + "browser_download_url": "https://github.com/pygame/stuntcat/releases/download/0.0.13/stuntcat-0.0.13-win32.msi", + "content_type": "application/octet-stream", + "created_at": "2019-01-05T11:04:28Z", + "download_count": 2, + "id": 10419315, + "label": "", + "name": "stuntcat-0.0.13-win32.msi", + "node_id": "MDEyOlJlbGVhc2VBc3NldDEwNDE5MzE1", + "size": 22106112, + "state": "uploaded", + "updated_at": "2019-01-05T11:04:30Z", + "uploader": { + "avatar_url": "https://avatars3.githubusercontent.com/u/9541?v=4", + "events_url": "https://api.github.com/users/illume/events{/privacy}", + "followers_url": "https://api.github.com/users/illume/followers", + "following_url": "https://api.github.com/users/illume/following{/other_user}", + "gists_url": "https://api.github.com/users/illume/gists{/gist_id}", + "gravatar_id": "", + "html_url": "https://github.com/illume", + "id": 9541, + "login": "illume", + "node_id": "MDQ6VXNlcjk1NDE=", + "organizations_url": "https://api.github.com/users/illume/orgs", + "received_events_url": "https://api.github.com/users/illume/received_events", + "repos_url": "https://api.github.com/users/illume/repos", + "site_admin": False, + "starred_url": "https://api.github.com/users/illume/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/illume/subscriptions", + "type": "User", + "url": "https://api.github.com/users/illume", + }, + "url": "https://api.github.com/repos/pygame/stuntcat/releases/assets/10419315", + }, + { + "browser_download_url": "https://github.com/pygame/stuntcat/releases/download/0.0.13/stuntcat-0.0.13.dmg", + "content_type": "application/x-apple-diskimage", + "created_at": "2019-01-05T11:08:36Z", + "download_count": 1, + "id": 10419329, + "label": "", + "name": "stuntcat-0.0.13.dmg", + "node_id": "MDEyOlJlbGVhc2VBc3NldDEwNDE5MzI5", + "size": 24125634, + "state": "uploaded", + "updated_at": "2019-01-05T11:08:37Z", + "uploader": { + "avatar_url": "https://avatars3.githubusercontent.com/u/9541?v=4", + "events_url": "https://api.github.com/users/illume/events{/privacy}", + "followers_url": "https://api.github.com/users/illume/followers", + "following_url": "https://api.github.com/users/illume/following{/other_user}", + "gists_url": "https://api.github.com/users/illume/gists{/gist_id}", + "gravatar_id": "", + "html_url": "https://github.com/illume", + "id": 9541, + "login": "illume", + "node_id": "MDQ6VXNlcjk1NDE=", + "organizations_url": "https://api.github.com/users/illume/orgs", + "received_events_url": "https://api.github.com/users/illume/received_events", + "repos_url": "https://api.github.com/users/illume/repos", + "site_admin": False, + "starred_url": "https://api.github.com/users/illume/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/illume/subscriptions", + "type": "User", + "url": "https://api.github.com/users/illume", + }, + "url": "https://api.github.com/repos/pygame/stuntcat/releases/assets/10419329", + }, + { + "browser_download_url": "https://github.com/pygame/stuntcat/releases/download/0.0.13/stuntcat-0.0.13.tar.gz", + "content_type": "application/gzip", + "created_at": "2019-01-05T11:08:38Z", + "download_count": 1, + "id": 10419331, + "label": "", + "name": "stuntcat-0.0.13.tar.gz", + "node_id": "MDEyOlJlbGVhc2VBc3NldDEwNDE5MzMx", + "size": 3084175, + "state": "uploaded", + "updated_at": "2019-01-05T11:08:38Z", + "uploader": { + "avatar_url": "https://avatars3.githubusercontent.com/u/9541?v=4", + "events_url": "https://api.github.com/users/illume/events{/privacy}", + "followers_url": "https://api.github.com/users/illume/followers", + "following_url": "https://api.github.com/users/illume/following{/other_user}", + "gists_url": "https://api.github.com/users/illume/gists{/gist_id}", + "gravatar_id": "", + "html_url": "https://github.com/illume", + "id": 9541, + "login": "illume", + "node_id": "MDQ6VXNlcjk1NDE=", + "organizations_url": "https://api.github.com/users/illume/orgs", + "received_events_url": "https://api.github.com/users/illume/received_events", + "repos_url": "https://api.github.com/users/illume/repos", + "site_admin": False, + "starred_url": "https://api.github.com/users/illume/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/illume/subscriptions", + "type": "User", + "url": "https://api.github.com/users/illume", + }, + "url": "https://api.github.com/repos/pygame/stuntcat/releases/assets/10419331", + }, + ], + "assets_url": "https://api.github.com/repos/pygame/stuntcat/releases/14815752/assets", + "author": { + "avatar_url": "https://avatars3.githubusercontent.com/u/9541?v=4", + "events_url": "https://api.github.com/users/illume/events{/privacy}", + "followers_url": "https://api.github.com/users/illume/followers", + "following_url": "https://api.github.com/users/illume/following{/other_user}", + "gists_url": "https://api.github.com/users/illume/gists{/gist_id}", + "gravatar_id": "", + "html_url": "https://github.com/illume", + "id": 9541, + "login": "illume", + "node_id": "MDQ6VXNlcjk1NDE=", + "organizations_url": "https://api.github.com/users/illume/orgs", + "received_events_url": "https://api.github.com/users/illume/received_events", + "repos_url": "https://api.github.com/users/illume/repos", + "site_admin": False, + "starred_url": "https://api.github.com/users/illume/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/illume/subscriptions", + "type": "User", + "url": "https://api.github.com/users/illume", + }, + "body": "This release can install via pip on a lot more machines. Because " + "pymunk binaries are now available for mac and linux.\r\n" + "', + "created_at": "2019-01-05T10:25:13Z", + "draft": False, + "html_url": "https://github.com/pygame/stuntcat/releases/tag/0.0.13", + "id": 14815752, + "name": "0.0.13", + "node_id": "MDc6UmVsZWFzZTE0ODE1NzUy", + "prerelease": False, + "published_at": "2019-01-06T15:29:18Z", + "tag_name": "0.0.13", + "tarball_url": "https://api.github.com/repos/pygame/stuntcat/tarball/0.0.13", + "target_commitish": "4f72f73ea65f108e6abff7cab1692c6de4307b31", + "upload_url": "https://uploads.github.com/repos/pygame/stuntcat/releases/14815752/assets{?name,label}", + "url": "https://api.github.com/repos/pygame/stuntcat/releases/14815752", + "zipball_url": "https://api.github.com/repos/pygame/stuntcat/zipball/0.0.13", + } + + +def test_sync_project(session, project): + """ when user account has been disabled. + """ + from pygameweb.project.models import Project, Tags, Release + from pygameweb.user.models import User, Group + from pygameweb.project.gh_releases import sync_project + + sync_project(session, project) + + +#TODO: test that for disabled users it does not update the releases. + +def test_release_from_gh(session, project, gh_release_atom, gh_release_api): + """ + """ + from pygameweb.project.gh_releases import release_from_gh + release = release_from_gh(session, project, gh_release_atom, gh_release_api) diff --git a/tests/functional/pygameweb/project/test_project_views.py b/tests/functional/pygameweb/project/test_project_views.py index c05ee99..f0ad4b6 100644 --- a/tests/functional/pygameweb/project/test_project_views.py +++ b/tests/functional/pygameweb/project/test_project_views.py @@ -77,6 +77,9 @@ def project(session, user): uri='http://some.example.com/', datetimeon=datetime.datetime(2017, 1, 5), image='1.png', + youtube_trailer='https://www.youtube.com/watch?v=8UnvMe1Neok', + github_repo='https://github.com/pygame/pygameweb/', + patreon='https://www.patreon.com/pygame', user=user ) @@ -303,10 +306,18 @@ def test_project_new(project_client, session, user): b'\x00\x00IEND\xaeB`\x82') image = (BytesIO(png), 'helloworld.png') - data = dict(image=image, title='title', version='1.0.2', - tags='tags', summary='summary', - description='description of project', - uri='http://example.com/') + data = dict( + image=image, + title='title', + version='1.0.2', + tags='tags', + summary='summary', + description='description of project', + uri='http://example.com/', + youtube_trailer='https://www.youtube.com/watch?v=8UnvMe1Neok', + github_repo='https://github.com/pygame/pygameweb/', + patreon='https://www.patreon.com/pygame', + ) with mock.patch('pygameweb.project.views.save_image') as save_image: resp = project_client.post('/members/projects/new', @@ -321,6 +332,10 @@ def test_project_new(project_client, session, user): resp = project_client.get(f'/project/{project.id}/') assert project.description.encode('utf8') in resp.data + project.youtube_trailer == data['youtube_trailer'] + project.github_repo == data['github_repo'] + project.patreon == data['patreon'] + assert resp.status_code == 200 assert project.title == 'title' assert project.releases[0].version == '1.0.2', 'a release was added too' @@ -403,6 +418,7 @@ def test_project_new(project_client, session, user): assert resp.status_code == 200 + def test_new_project_comment(project_client, session, project, project2, user): """ adds the thoughtful and supportive comment to the project page for the interesting creative work someone is trying to share with the world.