From 5be287b82ea044d8cbbc2fab547969add126f144 Mon Sep 17 00:00:00 2001 From: Miroslav Suchý Date: Mar 18 2014 12:09:48 +0000 Subject: move frontend into separate package --- diff --git a/copr.spec b/copr.spec index c6afb14..c88085f 100644 --- a/copr.spec +++ b/copr.spec @@ -1,4 +1,3 @@ -%global with_test 1 %global with_server 1 %if 0%{?rhel} < 7 && 0%{?rhel} > 0 %global _pkgdocdir %{_docdir}/%{name}-%{version} @@ -51,55 +50,6 @@ BuildRequires: policycoreutils >= %{POLICYCOREUTILSVER} COPR is lightweight build system. It allows you to create new project in WebUI, and submit new builds and COPR will create yum repository from latest builds. -%if %{with_server} -%package frontend -Summary: Frontend for COPR -Requires: httpd -Requires: mod_wsgi -Requires: python-alembic -Requires: python-flask -Requires: python-flask-openid -Requires: python-flask-wtf -Requires: python-flask-sqlalchemy -Requires: python-flask-script -Requires: python-flask-whooshee -#Requires: python-virtualenv -Requires: python-blinker -Requires: python-markdown -Requires: python-psycopg2 -Requires: python-pylibravatar -Requires: python-whoosh >= 2.5.3 -Requires: pytz -# for tests: -Requires: pytest -Requires: python-flexmock -Requires: python-decorator -Requires: yum -%if 0%{?rhel} < 7 && 0%{?rhel} > 0 -BuildRequires: python-argparse -%endif -# check -BuildRequires: python-flask -BuildRequires: python-flask-script -BuildRequires: python-flask-sqlalchemy -BuildRequires: python-flask-openid -BuildRequires: python-flask-whooshee -BuildRequires: python-pylibravatar -BuildRequires: python-flask-wtf -BuildRequires: pytest -BuildRequires: yum -BuildRequires: python-flexmock -BuildRequires: python-decorator -BuildRequires: python-markdown -BuildRequires: pytz - -%description frontend -COPR is lightweight build system. It allows you to create new project in WebUI, -and submit new builds and COPR will create yum repository from latests builds. - -This package contains frontend. -%endif # with_server - %package cli Summary: Command line interface for COPR Requires: python-requests @@ -174,24 +124,6 @@ popd %install -%if %{with_server} -#frontend -install -d %{buildroot}%{_sysconfdir} -install -d %{buildroot}%{_datadir}/copr/coprs_frontend -install -d %{buildroot}%{_sharedstatedir}/copr/data/openid_store -install -d %{buildroot}%{_sharedstatedir}/copr/data/openid_store/associations -install -d %{buildroot}%{_sharedstatedir}/copr/data/openid_store/nonces -install -d %{buildroot}%{_sharedstatedir}/copr/data/openid_store/temp -install -d %{buildroot}%{_sharedstatedir}/copr/data/whooshee -install -d %{buildroot}%{_sharedstatedir}/copr/data/whooshee/copr_user_whoosheer - -cp -a coprs_frontend/* %{buildroot}%{_datadir}/copr/coprs_frontend -mv %{buildroot}%{_datadir}/copr/coprs_frontend/coprs.conf.example ./ -mv %{buildroot}%{_datadir}/copr/coprs_frontend/config/* %{buildroot}%{_sysconfdir}/copr -rm %{buildroot}%{_datadir}/copr/coprs_frontend/CONTRIBUTION_GUIDELINES -touch %{buildroot}%{_sharedstatedir}/copr/data/copr.db -%endif # with_server - #copr-cli %{__python2} coprcli-setup.py install --root %{buildroot} install -d %{buildroot}%{_mandir}/man1 @@ -200,7 +132,6 @@ install -p -m 644 man/copr-cli.1 %{buildroot}/%{_mandir}/man1/ %if %{with_server} #doc cp -a documentation/python-doc %{buildroot}%{_pkgdocdir}/ -cp -a playbooks %{buildroot}%{_pkgdocdir}/ #selinux for selinuxvariant in targeted; do @@ -222,24 +153,7 @@ install -p -m 644 man/%{name}-selinux-enable.8 %{buildroot}/%{_mandir}/man8/ install -p -m 644 man/%{name}-selinux-relabel.8 %{buildroot}/%{_mandir}/man8/ %endif -%check -%if %{with_test} && %{with_server} && %{_arch} == "x86_64" - pushd coprs_frontend - rm -rf /tmp/copr.db /tmp/whooshee || : - COPR_CONFIG="$(pwd)/config/copr_unit_test.conf" ./manage.py test - popd -%endif - %if %{with_server} -%pre frontend -getent group copr-fe >/dev/null || groupadd -r copr-fe -getent passwd copr-fe >/dev/null || \ -useradd -r -g copr-fe -G copr-fe -d %{_datadir}/copr/coprs_frontend -s /bin/bash -c "COPR frontend user" copr-fe -/usr/bin/passwd -l copr-fe >/dev/null - -%post frontend -service httpd condrestart - %post selinux if /usr/sbin/selinuxenabled ; then %{_sbindir}/%{name}-selinux-enable @@ -260,25 +174,6 @@ if [ $1 -eq 0 ]; then fi %{sbinpath}/restorecon -rvvi %{_sharedstatedir}/copr -%files frontend -%doc LICENSE coprs.conf.example copr-setup.txt -%dir %{_datadir}/copr -%dir %{_sysconfdir}/copr -%dir %{_sharedstatedir}/copr -%{_datadir}/copr/coprs_frontend - -%defattr(-, copr-fe, copr-fe, -) -%dir %{_sharedstatedir}/copr/data -%dir %{_sharedstatedir}/copr/data/openid_store -%dir %{_sharedstatedir}/copr/data/whooshee -%dir %{_sharedstatedir}/copr/data/whooshee/copr_user_whoosheer - -%ghost %{_sharedstatedir}/copr/data/copr.db - -%defattr(600, copr-fe, copr-fe, 700) -%config(noreplace) %{_sysconfdir}/copr/copr.conf -%config(noreplace) %{_sysconfdir}/copr/copr_devel.conf -%config(noreplace) %{_sysconfdir}/copr/copr_unit_test.conf %endif # with_server %files cli diff --git a/coprs_frontend/CONTRIBUTION_GUIDELINES b/coprs_frontend/CONTRIBUTION_GUIDELINES deleted file mode 100644 index 0513e4e..0000000 --- a/coprs_frontend/CONTRIBUTION_GUIDELINES +++ /dev/null @@ -1,20 +0,0 @@ -This file contains some "should" rules, that are good to follow. - -- coprs.logic --- The *Logic objects should be named after the primary object that they - work with, but pluralized. E.g. CoprChroot->CoprChrootsLogic --- The methods of *Logic objects should generally be @classmethod. --- The methods of *Logic objects should accept "user" as a second argument - (after the "cls" argument). This argument should contain object of user - who is performing the action. --- The methods of *Logic objects shouldn't call db.session.commit(). This - should be called in views that use the methods. --- The usual names of methods are (each of the methods can perform certain - checks, e.g. authorization, correct parameters, ...): ---- "add" for creating objects and adding them to session ---- "new" for just adding objects to session ---- "get" for getting a query object for a single model object ---- "get_multiple" for getting a query object for multiple model objects ---- "edit" for editing objects and adding them to session ---- "update" for just adding altered objects to session ---- "delete" for deleting an object diff --git a/coprs_frontend/alembic.ini b/coprs_frontend/alembic.ini deleted file mode 100644 index 7c7be19..0000000 --- a/coprs_frontend/alembic.ini +++ /dev/null @@ -1,50 +0,0 @@ -# A generic, single database configuration. - -[alembic] -# path to migration scripts -script_location = alembic - -# template used to generate migration files -# file_template = %%(rev)s_%%(slug)s - -# set to 'true' to run the environment during -# the 'revision' command, regardless of autogenerate -# revision_environment = false - -# sqlalchemy.url = driver://user:pass@localhost/dbname - - -# Logging configuration -[loggers] -keys = root,sqlalchemy,alembic - -[handlers] -keys = console - -[formatters] -keys = generic - -[logger_root] -level = WARN -handlers = console -qualname = - -[logger_sqlalchemy] -level = WARN -handlers = -qualname = sqlalchemy.engine - -[logger_alembic] -level = INFO -handlers = -qualname = alembic - -[handler_console] -class = StreamHandler -args = (sys.stderr,) -level = NOTSET -formatter = generic - -[formatter_generic] -format = %(levelname)-5.5s [%(name)s] %(message)s -datefmt = %H:%M:%S diff --git a/coprs_frontend/alembic/env.py b/coprs_frontend/alembic/env.py deleted file mode 100644 index 530f4e6..0000000 --- a/coprs_frontend/alembic/env.py +++ /dev/null @@ -1,73 +0,0 @@ -from __future__ import with_statement -from alembic import context -from logging.config import fileConfig - -# this is the Alembic Config object, which provides -# access to the values within the .ini file in use. -config = context.config - -# Interpret the config file for Python logging. -# This line sets up loggers basically. -fileConfig(config.config_file_name) - -# add your model's MetaData object here -# for 'autogenerate' support -# from myapp import mymodel -# target_metadata = mymodel.Base.metadata -import sys -import os - -# alembic doesn't include cwd by default -sys.path.append(os.getcwd()) - -from coprs import db -target_metadata = db.metadata - -# other values from the config, defined by the needs of env.py, -# can be acquired: -# my_important_option = config.get_main_option("my_important_option") -# ... etc. - - -def run_migrations_offline(): - """Run migrations in 'offline' mode. - - This configures the context with just a URL - and not an Engine, though an Engine is acceptable - here as well. By skipping the Engine creation - we don't even need a DBAPI to be available. - - Calls to context.execute() here emit the given string to the - script output. - - """ - url = config.get_main_option("sqlalchemy.url") - context.configure(url=url) - - with context.begin_transaction(): - context.run_migrations() - - -def run_migrations_online(): - """Run migrations in 'online' mode. - - In this scenario we need to create an Engine - and associate a connection with the context. - - """ - connection = db.engine.connect() - context.configure( - connection=connection, - target_metadata=target_metadata - ) - - try: - with context.begin_transaction(): - context.run_migrations() - finally: - connection.close() - -if context.is_offline_mode(): - run_migrations_offline() -else: - run_migrations_online() diff --git a/coprs_frontend/alembic/script.py.mako b/coprs_frontend/alembic/script.py.mako deleted file mode 100644 index 9570201..0000000 --- a/coprs_frontend/alembic/script.py.mako +++ /dev/null @@ -1,22 +0,0 @@ -"""${message} - -Revision ID: ${up_revision} -Revises: ${down_revision} -Create Date: ${create_date} - -""" - -# revision identifiers, used by Alembic. -revision = ${repr(up_revision)} -down_revision = ${repr(down_revision)} - -from alembic import op -import sqlalchemy as sa -${imports if imports else ""} - -def upgrade(): - ${upgrades if upgrades else "pass"} - - -def downgrade(): - ${downgrades if downgrades else "pass"} diff --git a/coprs_frontend/alembic/versions/1ee4b45f5476_remove_fulltext_in_favor_of_whoosh.py b/coprs_frontend/alembic/versions/1ee4b45f5476_remove_fulltext_in_favor_of_whoosh.py deleted file mode 100644 index 21c0efc..0000000 --- a/coprs_frontend/alembic/versions/1ee4b45f5476_remove_fulltext_in_favor_of_whoosh.py +++ /dev/null @@ -1,31 +0,0 @@ -"""empty message - -Revision ID: 1ee4b45f5476 -Revises: 3a035889852c -Create Date: 2013-02-14 14:11:50.624673 - -""" - -# revision identifiers, used by Alembic. -revision = "1ee4b45f5476" -down_revision = "3a035889852c" - -from alembic import op -import sqlalchemy as sa - - -def upgrade(): - ### commands auto generated by Alembic - please adjust! ### - op.drop_column("copr", u"copr_ts_col") - if op.get_bind().dialect.name == "postgresql": - op.execute("DROP trigger IF EXISTS copr_ts_update ON copr") - elif op.get_bind().dialect.name == "sqlite": - op.execute("DROP trigger IF EXISTS copr_ts_update") - op.execute("DROP trigger IF EXISTS copr_ts_insert") - ### end Alembic commands ### - - -def downgrade(): - ### commands auto generated by Alembic - please adjust! ### - op.add_column("copr", sa.Column(u"copr_ts_col", sa.TEXT(), nullable=True)) - ### end Alembic commands ### diff --git a/coprs_frontend/alembic/versions/246fd2dbf398_add_legal_flag.py b/coprs_frontend/alembic/versions/246fd2dbf398_add_legal_flag.py deleted file mode 100644 index d8f4322..0000000 --- a/coprs_frontend/alembic/versions/246fd2dbf398_add_legal_flag.py +++ /dev/null @@ -1,36 +0,0 @@ -"""add_legal_flag - -Revision ID: 246fd2dbf398 -Revises: d062c3d9c00 -Create Date: 2013-04-03 10:39:54.837803 - -""" - -# revision identifiers, used by Alembic. -revision = "246fd2dbf398" -down_revision = "d062c3d9c00" - -from alembic import op -import sqlalchemy as sa - - -def upgrade(): - ### commands auto generated by Alembic - please adjust! ### - op.add_column( - "legal_flag", sa.Column("resolved_on", sa.Integer(), nullable=True)) - op.add_column( - "legal_flag", sa.Column("raised_on", sa.Integer(), nullable=True)) - op.drop_column("legal_flag", u"state") - op.drop_column("legal_flag", u"resolve_message") - ### end Alembic commands ### - - -def downgrade(): - ### commands auto generated by Alembic - please adjust! ### - op.add_column("legal_flag", sa.Column( - u"resolve_message", sa.TEXT(), nullable=True)) - op.add_column( - "legal_flag", sa.Column(u"state", sa.INTEGER(), nullable=True)) - op.drop_column("legal_flag", "raised_on") - op.drop_column("legal_flag", "resolved_on") - ### end Alembic commands ### diff --git a/coprs_frontend/alembic/versions/294405dfc7c0_add_action_data_fiel.py b/coprs_frontend/alembic/versions/294405dfc7c0_add_action_data_fiel.py deleted file mode 100644 index afe396b..0000000 --- a/coprs_frontend/alembic/versions/294405dfc7c0_add_action_data_fiel.py +++ /dev/null @@ -1,24 +0,0 @@ -"""add Action.data field - -Revision ID: 294405dfc7c0 -Revises: 3a415c6392bc -Create Date: 2014-01-20 15:43:09.986912 - -""" - -# revision identifiers, used by Alembic. -revision = "294405dfc7c0" -down_revision = "3a415c6392bc" - -from alembic import op -import sqlalchemy as sa - - -def upgrade(): - """ Add "data" colum to action table. """ - op.add_column("action", sa.Column("data", sa.Text())) - - -def downgrade(): - """ Drop "data" colum from action table. """ - op.drop_column("action", "data") diff --git a/coprs_frontend/alembic/versions/2a75f0a06d90_add_a_api_login_fiel.py b/coprs_frontend/alembic/versions/2a75f0a06d90_add_a_api_login_fiel.py deleted file mode 100644 index 980b707..0000000 --- a/coprs_frontend/alembic/versions/2a75f0a06d90_add_a_api_login_fiel.py +++ /dev/null @@ -1,26 +0,0 @@ -"""Add a api_login field to user - -Revision ID: 2a75f0a06d90 -Revises: 544873aa3ba1 -Create Date: 2013-03-10 10:01:16.820499 - -""" - -# revision identifiers, used by Alembic. -revision = "2a75f0a06d90" -down_revision = "544873aa3ba1" - -from alembic import op -import sqlalchemy as sa - - -def upgrade(): - """ Add the colum "api_login" to the table user. """ - op.add_column("user", sa.Column("api_login", sa.String(40), - nullable=False, - server_default="default_token")) - - -def downgrade(): - """ Drop the column "api_login" from the table user. """ - op.drop_column("user", "api_login") diff --git a/coprs_frontend/alembic/versions/2e30169e58ce_change_api_token_len.py b/coprs_frontend/alembic/versions/2e30169e58ce_change_api_token_len.py deleted file mode 100644 index 2e8c9bf..0000000 --- a/coprs_frontend/alembic/versions/2e30169e58ce_change_api_token_len.py +++ /dev/null @@ -1,30 +0,0 @@ -"""Change api_token length from varchar(40) to varchar(255) - -Revision ID: 2e30169e58ce -Revises: 32ba137a3d56 -Create Date: 2013-01-08 19:42:16.562926 - -""" - -# revision identifiers, used by Alembic. -revision = '2e30169e58ce' -down_revision = '32ba137a3d56' - -from alembic import op -import sqlalchemy as sa - - -def upgrade(): - """ Change the api_token field from the user table from varchar(40) to - varchar(255). - """ - if op.get_bind().dialect.name != 'sqlite': - op.alter_column("user", "api_token", type_=sa.String(255)) - - -def downgrade(): - """ Change the api_token field from the user table from varchar(255) to - varchar(40). - """ - if op.get_bind().dialect.name != 'sqlite': - op.alter_column("user", "api_token", type_=sa.String(40)) diff --git a/coprs_frontend/alembic/versions/2fa80e062525_add_mock_chroots.py b/coprs_frontend/alembic/versions/2fa80e062525_add_mock_chroots.py deleted file mode 100644 index 9b2e402..0000000 --- a/coprs_frontend/alembic/versions/2fa80e062525_add_mock_chroots.py +++ /dev/null @@ -1,111 +0,0 @@ -"""empty message - -Revision ID: 2fa80e062525 -Revises: 2e30169e58ce -Create Date: 2013-01-14 09:04:42.768432 - -""" - -# revision identifiers, used by Alembic. -revision = "2fa80e062525" -down_revision = "2e30169e58ce" - -from alembic import op -import sqlalchemy as sa - - -def upgrade(): - ### commands auto generated by Alembic - please adjust! ### - op.create_table("mock_chroot", - sa.Column("id", sa.Integer(), nullable=False), - sa.Column( - "os_release", sa.String(length=50), nullable=False), - sa.Column( - "os_version", sa.String(length=50), nullable=False), - sa.Column("arch", sa.String(length=50), nullable=False), - sa.Column("is_active", sa.Boolean(), nullable=False), - sa.PrimaryKeyConstraint("id") - ) - op.create_table("copr_chroot", - sa.Column("mock_chroot_id", sa.Integer(), nullable=False), - sa.Column("copr_id", sa.Integer(), nullable=False), - sa.ForeignKeyConstraint(["copr_id"], ["copr.id"], ), - sa.ForeignKeyConstraint( - ["mock_chroot_id"], ["mock_chroot.id"], ), - sa.PrimaryKeyConstraint("mock_chroot_id", "copr_id") - ) - - # transfer the data - we can"t assume how the code looks like when - # running the migration, so do everything from scratch - metadata = sa.MetaData() - # just what we need of copr table - coprs_table = sa.Table("copr", metadata, sa.Column( - "chroots", sa.Text()), sa.Column("id", sa.Integer())) - # get chroots - chroots = set() - for cs in op.get_bind().execute(sa.select([coprs_table.c.chroots])): - chroots.update(set(cs[0].split(" "))) - chroots = list(chroots) - - mc_table = sa.Table("mock_chroot", metadata, - sa.Column("id", sa.Integer(), nullable=False), - sa.Column( - "os_release", sa.String(length=50), nullable=False), - sa.Column( - "os_version", sa.String(length=50), nullable=False), - sa.Column( - "arch", sa.String(length=50), nullable=False), - sa.Column("is_active", sa.Boolean(), nullable=False), - ) - cc_table = sa.Table("copr_chroot", metadata, - sa.Column( - "mock_chroot_id", sa.Integer(), nullable=False), - sa.Column("copr_id", sa.Integer(), nullable=False), - ) - # each mock_chroot now has id of value i + 1 (not to include 0) - for i, c in enumerate(chroots): - sc = c.split("-") - op.bulk_insert(mc_table, [ - {"id": i + 1, - "os_release": sc[0], - "os_version": sc[1], - "arch": sc[2], - "is_active": True}]) - - # insert proper copr_chroots for every copr - for row in op.get_bind().execute(sa.select([coprs_table.c.id, coprs_table.c.chroots])): - for c in row[1].split(" "): - op.bulk_insert( - cc_table, [{"mock_chroot_id": chroots.index(c) + 1, - "copr_id": row[0]}]) - - if op.get_bind().dialect.name == "sqlite": - op.rename_table("copr", "copr_1") - op.create_table("copr", - sa.Column("id", sa.Integer(), nullable=False), - sa.Column( - "name", sa.String(length=100), nullable=False), - sa.Column("repos", sa.Text(), nullable=True), - sa.Column("created_on", sa.Integer(), nullable=True), - sa.Column("build_count", sa.Integer(), nullable=True), - sa.Column("owner_id", sa.Integer(), nullable=True), - sa.ForeignKeyConstraint(["owner_id"], ["user.id"], ), - sa.PrimaryKeyConstraint("id") - ) - op.execute( - "INSERT INTO copr SELECT id,name,repos,created_on,build_count,owner_id FROM copr_1") - op.drop_table("copr_1") - else: - op.drop_column("copr", u"chroots") - ### end Alembic commands ### - - -def downgrade(): - ### commands auto generated by Alembic - please adjust! ### - op.add_column("copr", sa.Column( - u"chroots", sa.TEXT(), nullable=False, - server_default="fedora-rawhide-x86_64")) - - op.drop_table("copr_chroot") - op.drop_table("mock_chroot") - ### end Alembic commands ### diff --git a/coprs_frontend/alembic/versions/32ba137a3d56_add_token_informatio.py b/coprs_frontend/alembic/versions/32ba137a3d56_add_token_informatio.py deleted file mode 100644 index f9f9161..0000000 --- a/coprs_frontend/alembic/versions/32ba137a3d56_add_token_informatio.py +++ /dev/null @@ -1,33 +0,0 @@ -"""Add token information to the user table - -Revision ID: 32ba137a3d56 -Revises: 595a31c145fb -Create Date: 2013-01-07 20:56:14.698735 - -""" - -# revision identifiers, used by Alembic. -revision = "32ba137a3d56" -down_revision = "595a31c145fb" - -from alembic import op -import sqlalchemy as sa - - -def upgrade(): - """ Add the coluns api_token and api_token_expiration to the user table. - """ - op.add_column("user", sa.Column("api_token", sa.String(40), - nullable=False, - server_default="default_token")) - - op.add_column("user", sa.Column("api_token_expiration", sa.Date, - nullable=False, - server_default="2000-1-1")) - - -def downgrade(): - """ Drop the coluns api_token and api_token_expiration to the user table. - """ - op.drop_column("user", "api_token") - op.drop_column("user", "api_token_expiration") diff --git a/coprs_frontend/alembic/versions/3a035889852c_add_copr_fulltext.py b/coprs_frontend/alembic/versions/3a035889852c_add_copr_fulltext.py deleted file mode 100644 index 36a94c8..0000000 --- a/coprs_frontend/alembic/versions/3a035889852c_add_copr_fulltext.py +++ /dev/null @@ -1,79 +0,0 @@ -"""add_copr_fulltext - -Revision ID: 3a035889852c -Revises: 3c3cce7a5fe0 -Create Date: 2013-02-01 10:06:37.034495 - -""" - -# revision identifiers, used by Alembic. -revision = '3a035889852c' -down_revision = '3c3cce7a5fe0' - -from alembic import op -import sqlalchemy as sa -from sqlalchemy import types -from sqlalchemy.ext import compiler - - -class Tsvector(types.UnicodeText): - pass - - -@compiler.compiles(Tsvector, 'postgresql') -def compile_tsvector(element, compiler, **kw): - return 'tsvector' - - -@compiler.compiles(Tsvector, 'sqlite') -def compile_tsvector(element, compiler, **kw): - return 'text' - - -def upgrade(): - ### commands auto generated by Alembic - please adjust! ### - op.add_column('copr', sa.Column('copr_ts_col', Tsvector(), nullable=True)) - op.create_index( - 'copr_ts_idx', 'copr', ['copr_ts_col'], postgresql_using='gin') - - session = sa.orm.sessionmaker(bind=op.get_bind())() - metadata = sa.MetaData() - if op.get_bind().dialect.name == 'postgresql': - op.execute("UPDATE copr \ - SET copr_ts_col = to_tsvector('pg_catalog.english', coalesce(name, '') || ' ' || \ - coalesce(description, '') || ' ' || coalesce(instructions, ''))") - # no need to coalesce here, the trigger doesn't need it - op.execute("CREATE TRIGGER copr_ts_update BEFORE INSERT OR UPDATE \ - ON copr \ - FOR EACH ROW EXECUTE PROCEDURE \ - tsvector_update_trigger(copr_ts_col, 'pg_catalog.english', name, description, instructions);") - elif op.get_bind().dialect.name == 'sqlite': - op.execute("UPDATE copr \ - SET copr_ts_col = coalesce(name, '') || ' ' || \ - coalesce(description, '') || ' ' || coalesce(instructions, '')") - # two triggers for sqlite... - op.execute("CREATE TRIGGER copr_ts_update \ - AFTER UPDATE OF name, description, instructions \ - ON copr \ - FOR EACH ROW \ - BEGIN \ - UPDATE copr SET copr_ts_col = coalesce(name, '') || ' ' || \ - coalesce(description, '') || ' ' || coalesce(instructions, ''); \ - END;") - op.execute("CREATE TRIGGER copr_ts_insert \ - AFTER INSERT \ - ON copr \ - FOR EACH ROW \ - BEGIN \ - UPDATE copr SET copr_ts_col = coalesce(name, '') || ' ' || \ - coalesce(description, '') || ' ' || coalesce(instructions, ''); \ - END;") - ### end Alembic commands ### - - -def downgrade(): - ### commands auto generated by Alembic - please adjust! ### - op.drop_column('copr', 'copr_ts_col') - if op.get_bind().dialect.name == 'postgresql': - op.execute("DROP TRIGGER copr_ts_update ON copr") - ### end Alembic commands ### diff --git a/coprs_frontend/alembic/versions/3a415c6392bc_add_buildroot_pkgs_c.py b/coprs_frontend/alembic/versions/3a415c6392bc_add_buildroot_pkgs_c.py deleted file mode 100644 index fc98197..0000000 --- a/coprs_frontend/alembic/versions/3a415c6392bc_add_buildroot_pkgs_c.py +++ /dev/null @@ -1,23 +0,0 @@ -"""add buildroot_pkgs column - -Revision ID: 3a415c6392bc -Revises: 52e53e7b413e -Create Date: 2013-11-28 15:46:24.860025 - -""" - -# revision identifiers, used by Alembic. -revision = "3a415c6392bc" -down_revision = "52e53e7b413e" - -from alembic import op -import sqlalchemy as sa - - -def upgrade(): - op.add_column("copr_chroot", sa.Column( - "buildroot_pkgs", sa.Text(), nullable=True)) - - -def downgrade(): - op.drop_column("copr_chroot", "buildroot_pkgs") diff --git a/coprs_frontend/alembic/versions/3c3cce7a5fe0_add_copr_desc_and_instruct.py b/coprs_frontend/alembic/versions/3c3cce7a5fe0_add_copr_desc_and_instruct.py deleted file mode 100644 index 77a612d..0000000 --- a/coprs_frontend/alembic/versions/3c3cce7a5fe0_add_copr_desc_and_instruct.py +++ /dev/null @@ -1,28 +0,0 @@ -"""empty message - -Revision ID: 3c3cce7a5fe0 -Revises: 2fa80e062525 -Create Date: 2013-01-22 09:42:39.037642 - -""" - -# revision identifiers, used by Alembic. -revision = "3c3cce7a5fe0" -down_revision = "2fa80e062525" - -from alembic import op -import sqlalchemy as sa - - -def upgrade(): - ### commands auto generated by Alembic - please adjust! ### - op.add_column("copr", sa.Column("description", sa.Text(), nullable=True)) - op.add_column("copr", sa.Column("instructions", sa.Text(), nullable=True)) - ### end Alembic commands ### - - -def downgrade(): - ### commands auto generated by Alembic - please adjust! ### - op.drop_column("copr", "instructions") - op.drop_column("copr", "description") - ### end Alembic commands ### diff --git a/coprs_frontend/alembic/versions/451e9507b866_generalize_action.py b/coprs_frontend/alembic/versions/451e9507b866_generalize_action.py deleted file mode 100644 index 3622cf4..0000000 --- a/coprs_frontend/alembic/versions/451e9507b866_generalize_action.py +++ /dev/null @@ -1,31 +0,0 @@ -"""generalize_action - -Revision ID: 451e9507b866 -Revises: 2a75f0a06d90 -Create Date: 2013-03-29 12:13:33.303584 - -""" - -# revision identifiers, used by Alembic. -revision = "451e9507b866" -down_revision = "2a75f0a06d90" - -from alembic import op -import sqlalchemy as sa - - -def upgrade(): - ### commands auto generated by Alembic - please adjust! ### - op.add_column("action", sa.Column("message", sa.Text(), nullable=True)) - op.add_column("action", sa.Column("ended_on", sa.Integer(), nullable=True)) - op.drop_column("action", u"backend_message") - ### end Alembic commands ### - - -def downgrade(): - ### commands auto generated by Alembic - please adjust! ### - op.add_column( - "action", sa.Column(u"backend_message", sa.TEXT(), nullable=True)) - op.drop_column("action", "ended_on") - op.drop_column("action", "message") - ### end Alembic commands ### diff --git a/coprs_frontend/alembic/versions/4837ad1d96ea_drop_copr_build_coun.py b/coprs_frontend/alembic/versions/4837ad1d96ea_drop_copr_build_coun.py deleted file mode 100644 index dc5e7ce..0000000 --- a/coprs_frontend/alembic/versions/4837ad1d96ea_drop_copr_build_coun.py +++ /dev/null @@ -1,24 +0,0 @@ -"""drop Copr.build_count - -Revision ID: 4837ad1d96ea -Revises: 294405dfc7c0 -Create Date: 2014-01-20 17:05:20.917522 - -""" - -# revision identifiers, used by Alembic. -revision = "4837ad1d96ea" -down_revision = "294405dfc7c0" - -from alembic import op -import sqlalchemy as sa - - -def upgrade(): - """ Drop "build_count" colum from copr table. """ - op.drop_column("copr", "build_count") - - -def downgrade(): - """ Add "build_count" colum to copr table. """ - op.add_column("copr", sa.Column("build_count", sa.Integer(default=0))) diff --git a/coprs_frontend/alembic/versions/498884ac47db_add_timezone_field.py b/coprs_frontend/alembic/versions/498884ac47db_add_timezone_field.py deleted file mode 100644 index add80d4..0000000 --- a/coprs_frontend/alembic/versions/498884ac47db_add_timezone_field.py +++ /dev/null @@ -1,24 +0,0 @@ -"""add timezone field - -Revision ID: 498884ac47db -Revises: 4837ad1d96ea -Create Date: 2014-01-23 12:15:04.450292 - -""" - -# revision identifiers, used by Alembic. -revision = '498884ac47db' -down_revision = '4837ad1d96ea' - -from alembic import op -import sqlalchemy as sa - - -def upgrade(): - """ Add 'data' colum to action table. """ - op.add_column('user', sa.Column('timezone', sa.String(length=50), nullable=True)) - - -def downgrade(): - """ Drop 'data' colum from action table. """ - op.drop_column('user', 'timezone') diff --git a/coprs_frontend/alembic/versions/52e53e7b413e_add_build_chroot.py b/coprs_frontend/alembic/versions/52e53e7b413e_add_build_chroot.py deleted file mode 100644 index f2bbebe..0000000 --- a/coprs_frontend/alembic/versions/52e53e7b413e_add_build_chroot.py +++ /dev/null @@ -1,75 +0,0 @@ -""" Add BuildChroot table - -Revision ID: 52e53e7b413e -Revises: 246fd2dbf398 -Create Date: 2013-11-14 09:00:43.787717 - -""" - -# revision identifiers, used by Alembic. -revision = "52e53e7b413e" -down_revision = "246fd2dbf398" - -from alembic import op -import sqlalchemy as sa - - -def upgrade(): - ### commands auto generated by Alembic - please adjust! ### - op.create_table("build_chroot", - sa.Column("mock_chroot_id", sa.Integer(), nullable=False), - sa.Column("build_id", sa.Integer(), nullable=False), - sa.Column("status", sa.Integer(), nullable=True), - sa.ForeignKeyConstraint(["build_id"], ["build.id"], ), - sa.ForeignKeyConstraint( - ["mock_chroot_id"], ["mock_chroot.id"], ), - sa.PrimaryKeyConstraint("mock_chroot_id", "build_id") - ) - - # transfer data from build table to build_chroot - metadata = sa.MetaData() - # just what we need of copr table - build_table = sa.Table("build", metadata, - sa.Column("chroots", sa.Text()), - sa.Column("status", sa.Integer()), - sa.Column("id", sa.Integer()), - ) - - mc_table = sa.Table("mock_chroot", metadata, - sa.Column("id", sa.Integer(), nullable=False), - sa.Column( - "os_release", sa.String(length=50), nullable=False), - sa.Column( - "os_version", sa.String(length=50), nullable=False), - sa.Column( - "arch", sa.String(length=50), nullable=False), - sa.Column("is_active", sa.Boolean(), nullable=False), - ) - bc_table = sa.Table("build_chroot", metadata, - sa.Column( - "mock_chroot_id", sa.Integer(), nullable=False), - sa.Column("build_id", sa.Integer(), nullable=False), - sa.Column("status", sa.Integer(), nullable=True), - ) - for row in op.get_bind().execute(sa.select([build_table.c.id, build_table.c.chroots, build_table.c.status])): - for c in row[1].split(" "): - chroot_array = c.split("-") - for row2 in (op.get_bind().execute(sa.select([mc_table.c.id], sa.and_( - mc_table.c.os_release == op.inline_literal(chroot_array[0]), - mc_table.c.os_version == op.inline_literal(chroot_array[1]), - mc_table.c.arch == op.inline_literal(chroot_array[2]), - )))): # should be just one row - op.bulk_insert( - bc_table, [{"mock_chroot_id": row2[0], "build_id": row[0], "status": row[2]}]) - - # drop old columns - op.drop_column(u"build", u"status") - op.drop_column(u"build", u"chroots") - - -def downgrade(): - print "Why are you downgrading? You will just lost some data." - op.add_column(u"build", sa.Column(u"chroots", sa.TEXT(), nullable=False)) - op.add_column(u"build", sa.Column(u"status", sa.INTEGER(), nullable=True)) - op.drop_table("build_chroot") - print "Data about chroots for builds are gone!" diff --git a/coprs_frontend/alembic/versions/544873aa3ba1_add_action.py b/coprs_frontend/alembic/versions/544873aa3ba1_add_action.py deleted file mode 100644 index dcacc6a..0000000 --- a/coprs_frontend/alembic/versions/544873aa3ba1_add_action.py +++ /dev/null @@ -1,40 +0,0 @@ -"""empty message - -Revision ID: 544873aa3ba1 -Revises: 1ee4b45f5476 -Create Date: 2013-02-20 13:20:34.778470 - -""" - -# revision identifiers, used by Alembic. -revision = "544873aa3ba1" -down_revision = "1ee4b45f5476" - -from alembic import op -import sqlalchemy as sa - - -def upgrade(): - ### commands auto generated by Alembic - please adjust! ### - op.create_table("action", - sa.Column("id", sa.Integer(), nullable=False), - sa.Column("action_type", sa.Integer(), nullable=False), - sa.Column( - "object_type", sa.String(length=20), nullable=True), - sa.Column("object_id", sa.Integer(), nullable=True), - sa.Column( - "old_value", sa.String(length=255), nullable=True), - sa.Column( - "new_value", sa.String(length=255), nullable=True), - sa.Column("backend_result", sa.Integer(), nullable=True), - sa.Column("backend_message", sa.Text(), nullable=True), - sa.Column("created_on", sa.Integer(), nullable=True), - sa.PrimaryKeyConstraint("id") - ) - ### end Alembic commands ### - - -def downgrade(): - ### commands auto generated by Alembic - please adjust! ### - op.drop_table("action") - ### end Alembic commands ### diff --git a/coprs_frontend/alembic/versions/595a31c145fb_initial_db_setup.py b/coprs_frontend/alembic/versions/595a31c145fb_initial_db_setup.py deleted file mode 100644 index 857e14c..0000000 --- a/coprs_frontend/alembic/versions/595a31c145fb_initial_db_setup.py +++ /dev/null @@ -1,77 +0,0 @@ -"""Initial DB setup - -Revision ID: 595a31c145fb -Revises: None -Create Date: 2012-11-26 09:39:51.229910 - -""" - -# revision identifiers, used by Alembic. -revision = "595a31c145fb" -down_revision = None - -from alembic import op -import sqlalchemy as sa - - -def upgrade(): - ### commands auto generated by Alembic - please adjust! ### - op.create_table("user", - sa.Column("id", sa.Integer(), nullable=False), - sa.Column( - "openid_name", sa.String(length=100), nullable=False), - sa.Column("mail", sa.String(length=150), nullable=False), - sa.Column("proven", sa.Boolean(), nullable=True), - sa.Column("admin", sa.Boolean(), nullable=True), - sa.PrimaryKeyConstraint("id") - ) - op.create_table("copr", - sa.Column("id", sa.Integer(), nullable=False), - sa.Column("name", sa.String(length=100), nullable=False), - sa.Column("chroots", sa.Text(), nullable=False), - sa.Column("repos", sa.Text(), nullable=True), - sa.Column("created_on", sa.Integer(), nullable=True), - sa.Column("build_count", sa.Integer(), nullable=True), - sa.Column("owner_id", sa.Integer(), nullable=True), - sa.ForeignKeyConstraint(["owner_id"], ["user.id"], ), - sa.PrimaryKeyConstraint("id") - ) - op.create_table("build", - sa.Column("id", sa.Integer(), nullable=False), - sa.Column("pkgs", sa.Text(), nullable=True), - sa.Column("canceled", sa.Boolean(), nullable=True), - sa.Column("chroots", sa.Text(), nullable=False), - sa.Column("repos", sa.Text(), nullable=True), - sa.Column("submitted_on", sa.Integer(), nullable=False), - sa.Column("started_on", sa.Integer(), nullable=True), - sa.Column("ended_on", sa.Integer(), nullable=True), - sa.Column("results", sa.Text(), nullable=True), - sa.Column("status", sa.Integer(), nullable=True), - sa.Column("memory_reqs", sa.Integer(), nullable=True), - sa.Column("timeout", sa.Integer(), nullable=True), - sa.Column("user_id", sa.Integer(), nullable=True), - sa.Column("copr_id", sa.Integer(), nullable=True), - sa.ForeignKeyConstraint(["copr_id"], ["copr.id"], ), - sa.ForeignKeyConstraint(["user_id"], ["user.id"], ), - sa.PrimaryKeyConstraint("id") - ) - op.create_table("copr_permission", - sa.Column( - "copr_builder", sa.SmallInteger(), nullable=True), - sa.Column("copr_admin", sa.SmallInteger(), nullable=True), - sa.Column("user_id", sa.Integer(), nullable=False), - sa.Column("copr_id", sa.Integer(), nullable=False), - sa.ForeignKeyConstraint(["copr_id"], ["copr.id"], ), - sa.ForeignKeyConstraint(["user_id"], ["user.id"], ), - sa.PrimaryKeyConstraint("user_id", "copr_id") - ) - ### end Alembic commands ### - - -def downgrade(): - ### commands auto generated by Alembic - please adjust! ### - op.drop_table("copr_permission") - op.drop_table("build") - op.drop_table("copr") - op.drop_table("user") - ### end Alembic commands ### diff --git a/coprs_frontend/alembic/versions/d062c3d9c00_backend_result_to_result.py b/coprs_frontend/alembic/versions/d062c3d9c00_backend_result_to_result.py deleted file mode 100644 index f335648..0000000 --- a/coprs_frontend/alembic/versions/d062c3d9c00_backend_result_to_result.py +++ /dev/null @@ -1,29 +0,0 @@ -"""backend_result_to_result - -Revision ID: d062c3d9c00 -Revises: 451e9507b866 -Create Date: 2013-04-03 10:10:35.990681 - -""" - -# revision identifiers, used by Alembic. -revision = "d062c3d9c00" -down_revision = "451e9507b866" - -from alembic import op -import sqlalchemy as sa - - -def upgrade(): - ### commands auto generated by Alembic - please adjust! ### - op.add_column("action", sa.Column("result", sa.Integer(), nullable=True)) - op.drop_column("action", u"backend_result") - ### end Alembic commands ### - - -def downgrade(): - ### commands auto generated by Alembic - please adjust! ### - op.add_column( - "action", sa.Column(u"backend_result", sa.INTEGER(), nullable=True)) - op.drop_column("action", "result") - ### end Alembic commands ### diff --git a/coprs_frontend/application b/coprs_frontend/application deleted file mode 100755 index 817870e..0000000 --- a/coprs_frontend/application +++ /dev/null @@ -1,12 +0,0 @@ -#!/usr/bin/python -import logging -import os -import sys - -# so that errors are not sent to stdout -logging.basicConfig(stream=sys.stderr) - -os.environ["COPRS_ENVIRON_PRODUCTION"] = "1" -sys.path.insert(0, os.path.dirname(__file__)) - -from coprs import app as application diff --git a/coprs_frontend/config/copr.conf b/coprs_frontend/config/copr.conf deleted file mode 100644 index e608897..0000000 --- a/coprs_frontend/config/copr.conf +++ /dev/null @@ -1,36 +0,0 @@ -# Directory and files where is stored Copr database files -#DATA_DIR = '/var/lib/copr/data' -#DATABASE = '/var/lib/copr/data/copr.db' -#OPENID_STORE = '/var/lib/copr/data/openid_store' -#WHOOSHEE_DIR = '/var/lib/copr/data/whooshee' - -# salt for CSRF codes -#SECRET_KEY = 'put_some_secret_here' - -#BACKEND_PASSWORD = 'password_here' - -# restrict access to a set of users -#USE_ALLOWED_USERS = False -#ALLOWED_USERS = ['bonnie', 'clyde'] - -SQLALCHEMY_DATABASE_URI = 'postgresql+psycopg2://copr-fe:coprpass@/coprdb' - -# Token length, defaults to 30 (max 255) -#API_TOKEN_LENGTH = 30 - -# Expiration of API token in days -#API_TOKEN_EXPIRATION = 180 - -# logging options -#SEND_LOGS_TO = ['root@localhost'] -#LOGGING_LEVEL = logging.ERROR - -# where to send notice about raised legal flag -#SEND_LEGAL_TO = ['root@localhost', 'somebody@somewhere.com'] - -DEBUG = False -SQLALCHEMY_ECHO = False - -#CSRF_ENABLED = True -# as of Flask-WTF 0.9+ -#WTF_CSRF_ENABLED = True diff --git a/coprs_frontend/config/copr_devel.conf b/coprs_frontend/config/copr_devel.conf deleted file mode 100644 index 2f139d1..0000000 --- a/coprs_frontend/config/copr_devel.conf +++ /dev/null @@ -1,33 +0,0 @@ -# Directory and files where is stored Copr database files -#DATA_DIR = '/var/lib/copr/data' -#DATABASE = '/var/lib/copr/data/copr.db' -#OPENID_STORE = '/var/lib/copr/data/openid_store' -#WHOOSHEE_DIR = '/var/lib/copr/data/whooshee' - -# salt for CSRF codes -#SECRET_KEY = 'put_some_secret_here' - -#BACKEND_PASSWORD = 'password_here' - -# restrict access to a set of users -#USE_ALLOWED_USERS = False -#ALLOWED_USERS = ['bonnie', 'clyde'] - -#SQLALCHEMY_DATABASE_URI = 'sqlite:////var/lib/copr/data/copr.db' - -# Token length, defaults to 30 (max 255) -#API_TOKEN_LENGTH = 30 - -# Expiration of API token in days -#API_TOKEN_EXPIRATION = 180 - -# logging options -#SEND_LOGS_TO = ['root@localhost'] -#LOGGING_LEVEL = logging.ERROR - -DEBUG = True -SQLALCHEMY_ECHO = True - -#CSRF_ENABLED = True -# as of Flask-WTF 0.9+ -#WTF_CSRF_ENABLED = True diff --git a/coprs_frontend/config/copr_unit_test.conf b/coprs_frontend/config/copr_unit_test.conf deleted file mode 100644 index e9de359..0000000 --- a/coprs_frontend/config/copr_unit_test.conf +++ /dev/null @@ -1,33 +0,0 @@ -# Directory and files where is stored Copr database files -DATA_DIR = '/tmp' -DATABASE = '/tmp/copr.db' -OPENID_STORE = '/tmp/openid_store' -WHOOSHEE_DIR = '/tmp/whooshee' - -# salt for CSRF codes -#SECRET_KEY = 'put_some_secret_here' - -#BACKEND_PASSWORD = 'password_here' - -# restrict access to a set of users -#USE_ALLOWED_USERS = False -#ALLOWED_USERS = ['bonnie', 'clyde'] - -SQLALCHEMY_DATABASE_URI = 'sqlite:///' + DATABASE - -# Token length, defaults to 30 (max 255) -#API_TOKEN_LENGTH = 30 - -# Expiration of API token in days -#API_TOKEN_EXPIRATION = 180 - -# logging options -#SEND_LOGS_TO = ['root@localhost'] -#LOGGING_LEVEL = logging.ERROR - -#DEBUG = False -#SQLALCHEMY_ECHO = False - -CSRF_ENABLED = False -# as of Flask-WTF 0.9+ -WTF_CSRF_ENABLED = False diff --git a/coprs_frontend/coprs.conf.example b/coprs_frontend/coprs.conf.example deleted file mode 100644 index 7162d5f..0000000 --- a/coprs_frontend/coprs.conf.example +++ /dev/null @@ -1,20 +0,0 @@ - - ServerName 127.0.0.1 - - #WSGIPassAuthorization On - WSGIDaemonProcess 127.0.0.1 user=copr-fe group=copr-fe threads=5 - WSGIScriptAlias / /usr/share/copr/coprs_frontend/application - WSGIProcessGroup 127.0.0.1 - - ErrorLog logs/error_coprs - CustomLog logs/access_coprs common - - - WSGIApplicationGroup %{GLOBAL} - # apache 2.2 (el6, F17) - #Order deny,allow - #Allow from all - # apache 2.4 (F18+) - Require all granted - - diff --git a/coprs_frontend/coprs/__init__.py b/coprs_frontend/coprs/__init__.py deleted file mode 100644 index 63192ab..0000000 --- a/coprs_frontend/coprs/__init__.py +++ /dev/null @@ -1,51 +0,0 @@ -from __future__ import with_statement - -import os -import flask - -from flask.ext.sqlalchemy import SQLAlchemy -from flask.ext.openid import OpenID -from flask.ext.whooshee import Whooshee - -app = flask.Flask(__name__) - -if "COPRS_ENVIRON_PRODUCTION" in os.environ: - app.config.from_object("coprs.config.ProductionConfig") -elif "COPRS_ENVIRON_UNITTEST" in os.environ: - app.config.from_object("coprs.config.UnitTestConfig") -else: - app.config.from_object("coprs.config.DevelopmentConfig") -if os.environ.get("COPR_CONFIG"): - app.config.from_envvar("COPR_CONFIG") -else: - app.config.from_pyfile("/etc/copr/copr.conf", silent=True) - - -oid = OpenID(app, app.config["OPENID_STORE"]) -db = SQLAlchemy(app) -whooshee = Whooshee(app) - -import coprs.filters -import coprs.log -import coprs.models -import coprs.whoosheers - -from coprs.views import admin_ns -from coprs.views.admin_ns import admin_general -from coprs.views import api_ns -from coprs.views.api_ns import api_general -from coprs.views import coprs_ns -from coprs.views.coprs_ns import coprs_builds -from coprs.views.coprs_ns import coprs_general -from coprs.views.coprs_ns import coprs_chroots -from coprs.views import backend_ns -from coprs.views.backend_ns import backend_general -from coprs.views import misc - -app.register_blueprint(api_ns.api_ns) -app.register_blueprint(admin_ns.admin_ns) -app.register_blueprint(coprs_ns.coprs_ns) -app.register_blueprint(misc.misc) -app.register_blueprint(backend_ns.backend_ns) - -app.add_url_rule("/", "coprs_ns.coprs_show", coprs_general.coprs_show) diff --git a/coprs_frontend/coprs/config.py b/coprs_frontend/coprs/config.py deleted file mode 100644 index 31b3618..0000000 --- a/coprs_frontend/coprs/config.py +++ /dev/null @@ -1,52 +0,0 @@ -import os -import logging - - -class Config(object): - DATA_DIR = os.path.join(os.path.dirname(__file__), "../../data") - DATABASE = os.path.join(DATA_DIR, "copr.db") - OPENID_STORE = os.path.join(DATA_DIR, "openid_store") - WHOOSHEE_DIR = os.path.join(DATA_DIR, "whooshee") - SECRET_KEY = "THISISNOTASECRETATALL" - BACKEND_PASSWORD = "thisisbackend" - - # restrict access to a set of users - USE_ALLOWED_USERS = False - ALLOWED_USERS = [] - - # SQLAlchemy - SQLALCHEMY_DATABASE_URI = "sqlite:///" + os.path.abspath(DATABASE) - - # Token length, defaults to 30, DB set to varchar 255 - API_TOKEN_LENGTH = 30 - - # Expiration of API token in days - API_TOKEN_EXPIRATION = 180 - - # logging options - SEND_LOGS_TO = ["root@localhost"] - LOGGING_LEVEL = logging.ERROR - - SEND_LEGAL_TO = ["root@localhost"] - - -class ProductionConfig(Config): - DEBUG = False - #SECRET_KEY = "put_some_secret_here" - #BACKEND_PASSWORD = "password_here" - #SQLALCHEMY_DATABASE_URI = "postgresql+psycopg2://login:password@/db_name" - - -class DevelopmentConfig(Config): - DEBUG = True - SQLALCHEMY_ECHO = True - - -class UnitTestConfig(Config): - CSRF_ENABLED = False - DATABASE = os.path.abspath("tests/data/copr.db") - OPENID_STORE = os.path.abspath("tests/data/openid_store") - WHOOSHEE_DIR = os.path.abspath("tests/data/whooshee") - - # SQLAlchemy - SQLALCHEMY_DATABASE_URI = "sqlite:///" + os.path.abspath(DATABASE) diff --git a/coprs_frontend/coprs/constants.py b/coprs_frontend/coprs/constants.py deleted file mode 100644 index a0709e0..0000000 --- a/coprs_frontend/coprs/constants.py +++ /dev/null @@ -1,27 +0,0 @@ -# Settings for chroots -INTEL_ARCHES = ["i386", "x86_64"] -DEFAULT_ARCHES = INTEL_ARCHES - -CHROOTS = { - "fedora-17": DEFAULT_ARCHES, - "fedora-18": DEFAULT_ARCHES, - "fedora-19": DEFAULT_ARCHES, - "fedora-20": DEFAULT_ARCHES, - "fedora-rawhide": DEFAULT_ARCHES, - "epel-5": DEFAULT_ARCHES, - "epel-6": DEFAULT_ARCHES, -} - -# PAGINATION -ITEMS_PER_PAGE = 10 -PAGES_URLS_COUNT = 5 - -# Builds defaults -## memory in MB -DEFAULT_BUILD_MEMORY = 2048 -MIN_BUILD_MEMORY = 2048 -MAX_BUILD_MEMORY = 4096 -# in seconds -DEFAULT_BUILD_TIMEOUT = 0 -MIN_BUILD_TIMEOUT = 0 -MAX_BUILD_TIMEOUT = 36000 diff --git a/coprs_frontend/coprs/exceptions.py b/coprs_frontend/coprs/exceptions.py deleted file mode 100644 index 8806c1c..0000000 --- a/coprs_frontend/coprs/exceptions.py +++ /dev/null @@ -1,34 +0,0 @@ -class ArgumentMissingException(BaseException): - pass - - -class MalformedArgumentException(ValueError): - pass - - -class NotFoundException(BaseException): - pass - - -class DuplicateException(BaseException): - pass - - -class InsufficientRightsException(BaseException): - pass - - -class ActionInProgressException(BaseException): - - def __init__(self, msg, action): - self.msg = msg - self.action = action - - def __unicode__(self): - return self.formatted_msg() - - def __str__(self): - return self.__unicode__() - - def formatted_msg(self): - return self.msg.format(action=self.action) diff --git a/coprs_frontend/coprs/filters.py b/coprs_frontend/coprs/filters.py deleted file mode 100644 index 6a3432e..0000000 --- a/coprs_frontend/coprs/filters.py +++ /dev/null @@ -1,66 +0,0 @@ -import datetime -import pytz -import time -import markdown - -from flask import Markup - -from coprs import app -from coprs import helpers - - -@app.template_filter("date_from_secs") -def date_from_secs(secs): - if secs: - return time.strftime("%Y-%m-%d %H:%M:%S %Z", time.gmtime(secs)) - - return None - - -@app.template_filter("perm_type_from_num") -def perm_type_from_num(num): - return helpers.PermissionEnum(num) - - -@app.template_filter("os_name_short") -def os_name_short(os_name, os_version): - # TODO: make it models.MockChroot method or not? - if os_version: - if os_version == "rawhide": - return os_version - if os_name == "fedora": - return "fc.{0}".format(os_version) - elif os_name == "epel": - return "el{0}".format(os_version) - return os_name - - -@app.template_filter('localized_time') -def localized_time(time_in, timezone): - """ return time shifted into timezone (and printed in ISO format) - - Input is in EPOCH (seconds since epoch). - """ - if not time_in: - return "Not yet" - format_tz = "%Y-%m-%d %H:%M:%S %Z" - utc_tz = pytz.timezone('UTC') - if timezone: - user_tz = pytz.timezone(timezone) - else: - user_tz = utc_tz - dt_aware = datetime.datetime.fromtimestamp(time_in).replace(tzinfo=utc_tz) - dt_my_tz = dt_aware.astimezone(user_tz) - return dt_my_tz.strftime(format_tz) - - -@app.template_filter("markdown") -def markdown_filter(data): - if not data: - return '' - - md = markdown.Markdown( - safe_mode="replace", - html_replacement_text="--RAW HTML NOT ALLOWED--") - - return Markup(md.convert(data)) diff --git a/coprs_frontend/coprs/forms.py b/coprs_frontend/coprs/forms.py deleted file mode 100644 index 6d77974..0000000 --- a/coprs_frontend/coprs/forms.py +++ /dev/null @@ -1,281 +0,0 @@ -import re -import urlparse - -import flask -import wtforms - -from flask.ext import wtf - -from coprs import constants -from coprs import helpers -from coprs import models -from coprs.logic import coprs_logic - - -class UrlListValidator(object): - - def __init__(self, message=None): - if not message: - message = "A list of URLs separated by whitespace characters" - " is needed ('{0}' doesn't seem to be a URL)." - self.message = message - - def __call__(self, form, field): - urls = field.data.split() - for u in urls: - if not self.is_url(u): - raise wtforms.ValidationError(self.message.format(u)) - - def is_url(self, url): - parsed = urlparse.urlparse(url) - is_url = True - - if not parsed.scheme.startswith("http"): - is_url = False - if not parsed.netloc: - is_url = False - - return is_url - - -class CoprUniqueNameValidator(object): - - def __init__(self, message=None): - if not message: - message = "You already have project named '{0}'." - self.message = message - - def __call__(self, form, field): - existing = coprs_logic.CoprsLogic.exists_for_user( - flask.g.user, field.data).first() - - if existing and str(existing.id) != form.id.data: - raise wtforms.ValidationError(self.message.format(field.data)) - - -class StringListFilter(object): - - def __call__(self, value): - if not value: - return '' - # Replace every whitespace string with one newline - # Formats ideally for html form filling, use replace('\n', ' ') - # to get space-separated values or split() to get list - result = value.strip() - regex = re.compile(r"\s+") - return regex.sub(lambda x: '\n', result) - - -class ValueToPermissionNumberFilter(object): - - def __call__(self, value): - if value: - return helpers.PermissionEnum("request") - return helpers.PermissionEnum("nothing") - - -class CoprFormFactory(object): - - @staticmethod - def create_form_cls(mock_chroots=None): - class F(wtf.Form): - # also use id here, to be able to find out whether user - # is updating a copr if so, we don't want to shout - # that name already exists - id = wtforms.HiddenField() - - name = wtforms.TextField( - "Name", - validators=[ - wtforms.validators.Required(), - wtforms.validators.Regexp( - re.compile(r"^[\w.-]+$"), - message="Name must contain only letters," - "digits, underscores, dashes and dots."), - CoprUniqueNameValidator() - ]) - - description = wtforms.TextAreaField("Description") - - instructions = wtforms.TextAreaField("Instructions") - - repos = wtforms.TextAreaField( - "Repos", - validators=[UrlListValidator()], - filters=[StringListFilter()]) - - initial_pkgs = wtforms.TextAreaField( - "Initial packages to build", - validators=[UrlListValidator()], - filters=[StringListFilter()]) - - @property - def selected_chroots(self): - selected = [] - for ch in self.chroots_list: - if getattr(self, ch).data: - selected.append(ch) - return selected - - def validate(self): - if not super(F, self).validate(): - return False - - if not self.validate_mock_chroots_not_empty(): - self._mock_chroots_error = "At least one chroot" \ - " must be selected" - return False - return True - - def validate_mock_chroots_not_empty(self): - have_any = False - for c in self.chroots_list: - if getattr(self, c).data: - have_any = True - return have_any - - F.chroots_list = map(lambda x: x.name, - models.MockChroot.query.filter( - models.MockChroot.is_active == True - ).all()) - F.chroots_list.sort() - # sets of chroots according to how we should print them in columns - F.chroots_sets = {} - for ch in F.chroots_list: - checkbox_default = False - if mock_chroots and ch in map(lambda x: x.name, - mock_chroots): - checkbox_default = True - - setattr(F, ch, wtforms.BooleanField(ch, default=checkbox_default)) - if ch[0] in F.chroots_sets: - F.chroots_sets[ch[0]].append(ch) - else: - F.chroots_sets[ch[0]] = [ch] - - return F - - -class CoprDeleteForm(wtf.Form): - verify = wtforms.TextField( - "Confirm deleting by typing 'yes'", - validators=[ - wtforms.validators.Required(), - wtforms.validators.Regexp( - r"^yes$", - message="Type 'yes' - without the quotes, lowercase.") - ]) - - -class BuildForm(wtf.Form): - pkgs = wtforms.TextAreaField( - "Pkgs", - validators=[ - wtforms.validators.Required(), - UrlListValidator()], - filters=[StringListFilter()]) - - memory_reqs = wtforms.IntegerField( - "Memory requirements", - validators=[ - wtforms.validators.NumberRange( - min=constants.MIN_BUILD_MEMORY, - max=constants.MAX_BUILD_MEMORY)], - default=constants.DEFAULT_BUILD_MEMORY) - - timeout = wtforms.IntegerField( - "Timeout", - validators=[ - wtforms.validators.NumberRange( - min=constants.MIN_BUILD_TIMEOUT, - max=constants.MAX_BUILD_TIMEOUT)], - default=constants.DEFAULT_BUILD_TIMEOUT) - - -class ChrootForm(wtf.Form): - - """ - Validator for editing chroots in project - (adding packages to minimal chroot) - """ - - buildroot_pkgs = wtforms.TextField( - "Additional packages to be always present in minimal buildroot") - - -class CoprLegalFlagForm(wtf.Form): - comment = wtforms.TextAreaField("Comment") - - -class PermissionsApplierFormFactory(object): - - @staticmethod - def create_form_cls(permission=None): - class F(wtf.Form): - pass - - builder_default = False - admin_default = False - - if permission: - if permission.copr_builder != helpers.PermissionEnum("nothing"): - builder_default = True - if permission.copr_admin != helpers.PermissionEnum("nothing"): - admin_default = True - - setattr(F, "copr_builder", - wtforms.BooleanField( - default=builder_default, - filters=[ValueToPermissionNumberFilter()])) - - setattr(F, "copr_admin", - wtforms.BooleanField( - default=admin_default, - filters=[ValueToPermissionNumberFilter()])) - - return F - - -class PermissionsFormFactory(object): - - """Creates a dynamic form for given set of copr permissions""" - @staticmethod - def create_form_cls(permissions): - class F(wtf.Form): - pass - - for perm in permissions: - builder_choices = helpers.PermissionEnum.choices_list() - admin_choices = helpers.PermissionEnum.choices_list() - - builder_default = perm.copr_builder - admin_default = perm.copr_admin - - setattr(F, "copr_builder_{0}".format(perm.user.id), - wtforms.SelectField( - choices=builder_choices, - default=builder_default, - coerce=int)) - - setattr(F, "copr_admin_{0}".format(perm.user.id), - wtforms.SelectField( - choices=admin_choices, - default=admin_default, - coerce=int)) - - return F - -class CoprModifyForm(wtf.Form): - description = wtforms.TextAreaField('Description', - validators=[wtforms.validators.Optional()]) - - instructions = wtforms.TextAreaField('Instructions', - validators=[wtforms.validators.Optional()]) - - repos = wtforms.TextAreaField('Repos', - validators=[UrlListValidator(), - wtforms.validators.Optional()], - filters=[StringListFilter()]) - -class ModifyChrootForm(wtf.Form): - buildroot_pkgs = wtforms.TextField('Additional packages to be always present in minimal buildroot') diff --git a/coprs_frontend/coprs/helpers.py b/coprs_frontend/coprs/helpers.py deleted file mode 100644 index 2074bdb..0000000 --- a/coprs_frontend/coprs/helpers.py +++ /dev/null @@ -1,227 +0,0 @@ -import math -import random -import string -import urlparse -import flask - -from coprs import constants - -from rpmUtils.miscutils import splitFilename - - -def generate_api_token(size=30): - """ Generate a random string used as token to access the API - remotely. - - :kwarg: size, the size of the token to generate, defaults to 30 - chars. - :return: a string, the API token for the user. - """ - return ''.join(random.choice(string.ascii_lowercase) for x in range(size)) - - -class EnumType(type): - - def __call__(self, attr): - if isinstance(attr, int): - for k, v in self.vals.items(): - if v == attr: - return k - raise KeyError("num {0} is not mapped".format(attr)) - else: - return self.vals[attr] - - -class PermissionEnum(object): - __metaclass__ = EnumType - vals = {"nothing": 0, "request": 1, "approved": 2} - - @classmethod - def choices_list(cls, without=-1): - return [(n, k) for k, n in cls.vals.items() if n != without] - - -class ActionTypeEnum(object): - __metaclass__ = EnumType - vals = {"delete": 0, "rename": 1, "legal-flag": 2} - - -class BackendResultEnum(object): - __metaclass__ = EnumType - vals = {"waiting": 0, "success": 1, "failure": 2} - - -class RoleEnum(object): - __metaclass__ = EnumType - vals = {"user": 0, "admin": 1} - - -class StatusEnum(object): - __metaclass__ = EnumType - vals = {"failed": 0, - "succeeded": 1, - "canceled": 2, - "running": 3, - "pending": 4} - - -class Paginator(object): - - def __init__(self, query, total_count, page=1, - per_page_override=None, urls_count_override=None): - - self.query = query - self.total_count = total_count - self.page = page - self.per_page = per_page_override or constants.ITEMS_PER_PAGE - self.urls_count = urls_count_override or constants.PAGES_URLS_COUNT - self._sliced_query = None - - def page_slice(self, page): - return (constants.ITEMS_PER_PAGE * (page - 1), - constants.ITEMS_PER_PAGE * page) - - @property - def sliced_query(self): - if not self._sliced_query: - self._sliced_query = self.query[slice(*self.page_slice(self.page))] - return self._sliced_query - - @property - def pages(self): - return int(math.ceil(self.total_count / float(self.per_page))) - - def border_url(self, request, start): - if start: - if self.page - 1 > self.urls_count / 2: - return (self.url_for_other_page(request, 1), 1) - else: - if self.page < self.pages - self.urls_count / 2: - return (self.url_for_other_page(request, self.pages), - self.pages) - - return None - - def get_urls(self, request): - left_border = self.page - self.urls_count / 2 - left_border = 1 if left_border < 1 else left_border - right_border = self.page + self.urls_count / 2 - right_border = self.pages if right_border > self.pages else right_border - - return [(self.url_for_other_page(request, i), i) - for i in range(left_border, right_border + 1)] - - def url_for_other_page(self, request, page): - args = request.view_args.copy() - args["page"] = page - return flask.url_for(request.endpoint, **args) - - -def parse_package_name(pkg): - """ - Parse package name from possibly incomplete nvra string. - """ - - if pkg.count(".") >= 3 and pkg.count("-") >= 2: - return splitFilename(pkg)[0] - - # doesn"t seem like valid pkg string, try to guess package name - result = "" - pkg = pkg.replace(".rpm", "").replace(".src", "") - - for delim in ["-", "."]: - if delim in pkg: - parts = pkg.split(delim) - for part in parts: - if any(map(lambda x: x.isdigit(), part)): - return result[:-1] - - result += part + "-" - - return result[:-1] - - return pkg - - -def render_repo(copr, mock_chroot, url): - """ Render .repo file. No checks if copr or mock_chroot exists. """ - if mock_chroot.os_release == "fedora": - if mock_chroot.os_version != "rawhide": - mock_chroot.os_version = "$releasever" - - url = urlparse.urljoin( - url, "{0}-{1}-{2}/".format(mock_chroot.os_release, - mock_chroot.os_version, "$basearch")) - - #url = url.replace("http://", "https://") - return flask.render_template("coprs/copr.repo", copr=copr, url=url) - - -class Serializer(object): - - def to_dict(self, options={}): - """ - Usage: - - SQLAlchObject.to_dict() => returns a flat dict of the object - SQLAlchObject.to_dict({"foo": {}}) => returns a dict of the object - and will include a flat dict of object foo inside of that - SQLAlchObject.to_dict({"foo": {"bar": {}}, "spam": {}}) => returns - a dict of the object, which will include dict of foo - (which will include dict of bar) and dict of spam. - - Options can also contain two special values: __columns_only__ - and __columns_except__ - - If present, the first makes only specified fiels appear, - the second removes specified fields. Both of these fields - must be either strings (only works for one field) or lists - (for one and more fields). - - SQLAlchObject.to_dict({"foo": {"__columns_except__": ["id"]}, - "__columns_only__": "name"}) => - - The SQLAlchObject will only put its "name" into the resulting dict, - while "foo" all of its fields except "id". - - Options can also specify whether to include foo_id when displaying - related foo object (__included_ids__, defaults to True). - This doesn"t apply when __columns_only__ is specified. - """ - - result = {} - columns = self.serializable_attributes - - if "__columns_only__" in options: - columns = options["__columns_only__"] - else: - columns = set(columns) - if "__columns_except__" in options: - columns_except = options["__columns_except__"] - if not isinstance(options["__columns_except__"], list): - columns_except = [options["__columns_except__"]] - - columns -= set(columns_except) - - if ("__included_ids__" in options and - options["__included_ids__"] is False): - - related_objs_ids = [ - r + "_id" for r, o in options.items() - if not r.startswith("__")] - - columns -= set(related_objs_ids) - - columns = list(columns) - - for column in columns: - result[column] = getattr(self, column) - - for related, values in options.items(): - if hasattr(self, related): - result[related] = getattr(self, related).to_dict(values) - return result - - @property - def serializable_attributes(self): - return map(lambda x: x.name, self.__table__.columns) diff --git a/coprs_frontend/coprs/log.py b/coprs_frontend/coprs/log.py deleted file mode 100644 index 2f05da3..0000000 --- a/coprs_frontend/coprs/log.py +++ /dev/null @@ -1,31 +0,0 @@ -import logging -import logging.handlers - -from coprs import app - -send_logs_to = app.config.get("SEND_LOGS_TO") -level = app.config.get("LOGGING_LEVEL") - -formatter = logging.Formatter(""" -Message type: %(levelname)s -Location: %(pathname)s:%(lineno)d -Module: %(module)s -Function: %(funcName)s -Time: %(asctime)s - -Message: - -%(message)s -""") - -if not app.debug: - mail_handler = logging.handlers.SMTPHandler( - "127.0.0.1", - "copr-fe-error@{0}".format( - app.config["SERVER_NAME"] or "fedorahosted.org"), - send_logs_to, - "Yay, error in copr frontend occured!") - - mail_handler.setFormatter(formatter) - mail_handler.setLevel(level) - app.logger.addHandler(mail_handler) diff --git a/coprs_frontend/coprs/logic/__init__.py b/coprs_frontend/coprs/logic/__init__.py deleted file mode 100644 index e69de29..0000000 --- a/coprs_frontend/coprs/logic/__init__.py +++ /dev/null diff --git a/coprs_frontend/coprs/logic/actions_logic.py b/coprs_frontend/coprs/logic/actions_logic.py deleted file mode 100644 index b77079e..0000000 --- a/coprs_frontend/coprs/logic/actions_logic.py +++ /dev/null @@ -1,53 +0,0 @@ -from coprs import db -from coprs import models -from coprs import helpers - - -class ActionsLogic(object): - - @classmethod - def get(cls, action_id): - """ - Return single action identified by `action_id` - """ - - query = models.Action.query.filter(models.Action.id == action_id) - return query - - @classmethod - def get_waiting(cls): - """ - Return actions that aren't finished - """ - - query = (models.Action.query - .filter(models.Action.result == - helpers.BackendResultEnum("waiting")) - .filter(models.Action.action_type != - helpers.ActionTypeEnum("legal-flag")) - .order_by(models.Action.created_on.asc())) - - return query - - @classmethod - def get_by_ids(cls, ids): - """ - Return actions matching passed `ids` - """ - - return models.Action.query.filter(models.Action.id.in_(ids)) - - @classmethod - def update_state_from_dict(cls, action, upd_dict): - """ - Update `action` object with `upd_dict` data - - Updates result, message and ended_on parameters. - """ - - for attr in ["result", "message", "ended_on"]: - value = upd_dict.get(attr, None) - if value: - setattr(action, attr, value) - - db.session.add(action) diff --git a/coprs_frontend/coprs/logic/builds_logic.py b/coprs_frontend/coprs/logic/builds_logic.py deleted file mode 100644 index a6e234c..0000000 --- a/coprs_frontend/coprs/logic/builds_logic.py +++ /dev/null @@ -1,183 +0,0 @@ -import time - -from coprs import db -from coprs import exceptions -from coprs import models -from coprs import helpers -from coprs import signals - -from coprs.logic import coprs_logic -from coprs.logic import users_logic - - -class BuildsLogic(object): - - @classmethod - def get(cls, build_id): - query = models.Build.query.filter(models.Build.id == build_id) - return query - - @classmethod - def get_multiple(cls, user, **kwargs): - copr = kwargs.get("copr", None) - username = kwargs.get("username", None) - coprname = kwargs.get("coprname", None) - - query = models.Build.query.order_by(models.Build.submitted_on.desc()) - - # if we get copr, query by its id - if copr: - query = query.filter(models.Build.copr == copr) - elif username and coprname: - query = (query.join(models.Build.copr) - .options(db.contains_eager(models.Build.copr)) - .join(models.Copr.owner) - .filter(models.Copr.name == coprname) - .filter(models.User.openid_name == - models.User.openidize_name(username)) - .order_by(models.Build.submitted_on.desc())) - else: - raise exceptions.ArgumentMissingException( - "Must pass either copr or both coprname and username") - - return query - - @classmethod - def get_waiting(cls): - """ - Return builds that aren't both started and finished - (if build start submission fails, we still want to mark - the build as non-waiting, if it ended) - this has very different goal then get_multiple, so implement it alone - """ - - query = (models.Build.query.join(models.Build.copr) - .join(models.User) - .options(db.contains_eager(models.Build.copr)) - .options(db.contains_eager("copr.owner")) - .filter((models.Build.started_on == None) - | (models.Build.started_on < int(time.time() - 7200))) - .filter(models.Build.ended_on == None) - .filter(models.Build.canceled != True) - .order_by(models.Build.submitted_on.asc())) - return query - - @classmethod - def get_by_ids(cls, ids): - return models.Build.query.filter(models.Build.id.in_(ids)) - - @classmethod - def add(cls, user, pkgs, copr, - repos=None, memory_reqs=None, timeout=None): - - coprs_logic.CoprsLogic.raise_if_unfinished_blocking_action( - user, copr, - "Can't build while there is an operation in progress: {action}") - users_logic.UsersLogic.raise_if_cant_build_in_copr( - user, copr, - "You don't have permissions to build in this copr.") - - if not repos: - repos = copr.repos - - build = models.Build( - user=user, - pkgs=pkgs, - copr=copr, - repos=repos, - submitted_on=int(time.time())) - - if memory_reqs: - build.memory_reqs = memory_reqs - - if timeout: - build.timeout = timeout - - db.session.add(build) - - # add BuildChroot object for each active chroot - # this copr is assigned to - for chroot in copr.active_chroots: - buildchroot = models.BuildChroot( - build=build, - mock_chroot=chroot) - - db.session.add(buildchroot) - - return build - - @classmethod - def update_state_from_dict(cls, build, upd_dict): - if "chroot" in upd_dict: - # update respective chroot status - for build_chroot in build.build_chroots: - if build_chroot.name == upd_dict["chroot"]: - if "status" in upd_dict: - build_chroot.status = upd_dict["status"] - - db.session.add(build_chroot) - - for attr in ["results", "started_on", "ended_on"]: - value = upd_dict.get(attr, None) - if value: - # only update started_on once - if attr == "started_on" and build.started_on: - continue - - # only update ended_on and results - # when there are no pending builds - if (attr in ["ended_on", "results"] and - build.has_pending_chroot): - continue - - if attr == "ended_on": - signals.build_finished.send(cls, build=build) - - setattr(build, attr, value) - - db.session.add(build) - - @classmethod - def cancel_build(cls, user, build): - if not (user.can_build_in(build.copr)): - raise exceptions.InsufficientRightsException( - "You are not allowed to cancel this build.") - build.canceled = True - - @classmethod - def delete_build(cls, user, build): - if not (user.can_build_in(build.copr)): - raise exceptions.InsufficientRightsException( - "You are not allowed to delete this build.") - - action = models.Action(action_type=helpers.ActionTypeEnum("delete"), - object_type="build", - object_id=build.id, - old_value="{0}/{1}".format(build.copr.owner.name, - build.copr.name), - data=build.pkgs, - created_on=int(time.time())) - - db.session.add(action) - for build_chroot in build.build_chroots: - db.session.delete(build_chroot) - db.session.delete(build) - - @classmethod - def last_modified(cls, copr): - """ Get build datetime (as epoch) of last successfull build - - :arg copr: object of copr - """ - builds = cls.get_multiple(None, copr=copr) - - last_build = (builds - .join(models.BuildChroot) - .filter(models.BuildChroot.status == helpers.StatusEnum("succeeded")) - .filter(models.Build.ended_on != None) - .order_by(models.Build.ended_on.desc()) - ).first() - if last_build: - return last_build.ended_on - else: - return None diff --git a/coprs_frontend/coprs/logic/coprs_logic.py b/coprs_frontend/coprs/logic/coprs_logic.py deleted file mode 100644 index 7cab264..0000000 --- a/coprs_frontend/coprs/logic/coprs_logic.py +++ /dev/null @@ -1,435 +0,0 @@ -import time - -from coprs import db -from coprs import exceptions -from coprs import helpers -from coprs import models -from coprs import signals -from coprs.logic import users_logic - -class CoprsLogic(object): - - """ - Used for manipulating Coprs. - - All methods accept user object as a first argument, - as this may be needed in future. - """ - - @classmethod - def get(cls, user, username, coprname, **kwargs): - with_builds = kwargs.get("with_builds", False) - with_mock_chroots = kwargs.get("with_mock_chroots", False) - - query = (db.session.query(models.Copr) - .join(models.Copr.owner) - .options(db.contains_eager(models.Copr.owner)) - .filter(models.Copr.name == coprname) - .filter(models.User.openid_name == - models.User.openidize_name(username)) - .filter(models.Copr.deleted == False)) - - if with_builds: - query = (query.outerjoin(models.Copr.builds) - .options(db.contains_eager(models.Copr.builds)) - .order_by(models.Build.submitted_on.desc())) - - if with_mock_chroots: - query = (query.outerjoin(*models.Copr.mock_chroots.attr) - .options(db.contains_eager(*models.Copr.mock_chroots.attr)) - .order_by(models.MockChroot.os_release.asc()) - .order_by(models.MockChroot.os_version.asc()) - .order_by(models.MockChroot.arch.asc())) - - return query - - @classmethod - def get_multiple(cls, user, **kwargs): - user_relation = kwargs.get("user_relation", None) - username = kwargs.get("username", None) - with_mock_chroots = kwargs.get("with_mock_chroots", None) - with_builds = kwargs.get("with_builds", None) - incl_deleted = kwargs.get("incl_deleted", None) - ids = kwargs.get("ids", None) - - query = (db.session.query(models.Copr) - .join(models.Copr.owner) - .options(db.contains_eager(models.Copr.owner)) - .order_by(models.Copr.id.desc())) - - if not incl_deleted: - query = query.filter(models.Copr.deleted == False) - - if isinstance(ids, list): # can be an empty list - query = query.filter(models.Copr.id.in_(ids)) - - if user_relation == "owned": - query = query.filter( - models.User.openid_name == models.User.openidize_name(username)) - elif user_relation == "allowed": - aliased_user = db.aliased(models.User) - - query = (query.join(models.CoprPermission, - models.Copr.copr_permissions) - .filter(models.CoprPermission.copr_builder == - helpers.PermissionEnum('approved')) - .join(aliased_user, models.CoprPermission.user) - .filter(aliased_user.openid_name == - models.User.openidize_name(username))) - - if with_mock_chroots: - query = (query.outerjoin(*models.Copr.mock_chroots.attr) - .options(db.contains_eager(*models.Copr.mock_chroots.attr)) - .order_by(models.MockChroot.os_release.asc()) - .order_by(models.MockChroot.os_version.asc()) - .order_by(models.MockChroot.arch.asc())) - - if with_builds: - query = (query.outerjoin(models.Copr.builds) - .options(db.contains_eager(models.Copr.builds)) - .order_by(models.Build.submitted_on.desc())) - - return query - - @classmethod - def get_multiple_fulltext(cls, user, search_string): - query = (models.Copr.query.join(models.User) - .filter(models.Copr.deleted == False) - .whooshee_search(search_string)) - return query - - @classmethod - def add(cls, user, name, repos, selected_chroots, description, - instructions, check_for_duplicates=False): - copr = models.Copr(name=name, - repos=repos, - owner=user, - description=description, - instructions=instructions, - created_on=int(time.time())) - - # form validation checks for duplicates - CoprsLogic.new(user, copr, - check_for_duplicates=check_for_duplicates) - CoprChrootsLogic.new_from_names(user, copr, - selected_chroots) - return copr - - @classmethod - def new(cls, user, copr, check_for_duplicates=True): - if check_for_duplicates and cls.exists_for_user(user, copr.name).all(): - raise exceptions.DuplicateException( - "Copr: '{0}' already exists".format(copr.name)) - signals.copr_created.send(cls, copr=copr) - db.session.add(copr) - - @classmethod - def update(cls, user, copr, check_for_duplicates=True): - cls.raise_if_unfinished_blocking_action( - user, copr, "Can't change this project name," - " another operation is in progress: {action}") - - users_logic.UsersLogic.raise_if_cant_update_copr( - user, copr, "Only owners and admins may update their projects.") - - existing = cls.exists_for_user(copr.owner, copr.name).first() - if existing: - if check_for_duplicates and existing.id != copr.id: - raise exceptions.DuplicateException( - "Project: '{0}' already exists".format(copr.name)) - - else: # we're renaming - # if we fire a models.Copr.query, it will use the modified copr in session - # -> workaround this by just getting the name - old_copr_name = (db.session.query(models.Copr.name) - .filter(models.Copr.id == copr.id) - .filter(models.Copr.deleted == False) - .first()[0]) - - action = models.Action(action_type=helpers.ActionTypeEnum("rename"), - object_type="copr", - object_id=copr.id, - old_value="{0}/{1}".format(copr.owner.name, - old_copr_name), - new_value="{0}/{1}".format(copr.owner.name, - copr.name), - created_on=int(time.time())) - db.session.add(action) - db.session.add(copr) - - @classmethod - def delete(cls, user, copr, check_for_duplicates=True): - cls.raise_if_cant_delete(user, copr) - # TODO: do we want to dump the information somewhere, so that we can - # search it in future? - cls.raise_if_unfinished_blocking_action( - user, copr, "Can't delete this project," - " another operation is in progress: {action}") - - action = models.Action(action_type=helpers.ActionTypeEnum("delete"), - object_type="copr", - object_id=copr.id, - old_value="{0}/{1}".format(copr.owner.name, - copr.name), - new_value="", - created_on=int(time.time())) - copr.deleted = True - - db.session.add(action) - - return copr - - @classmethod - def exists_for_user(cls, user, coprname, incl_deleted=False): - existing = (models.Copr.query - .filter(models.Copr.name == coprname) - .filter(models.Copr.owner_id == user.id)) - - if not incl_deleted: - existing = existing.filter(models.Copr.deleted == False) - - return existing - - @classmethod - def unfinished_blocking_actions_for(cls, user, copr): - blocking_actions = [helpers.ActionTypeEnum("rename"), - helpers.ActionTypeEnum("delete")] - - actions = (models.Action.query - .filter(models.Action.object_type == "copr") - .filter(models.Action.object_id == copr.id) - .filter(models.Action.result == - helpers.BackendResultEnum("waiting")) - .filter(models.Action.action_type.in_(blocking_actions))) - - return actions - - @classmethod - def raise_if_unfinished_blocking_action(cls, user, copr, message): - """ - Raise ActionInProgressException if given copr has an unfinished - action. Return None otherwise. - """ - - unfinished_actions = cls.unfinished_blocking_actions_for( - user, copr).all() - if unfinished_actions: - raise exceptions.ActionInProgressException( - message, unfinished_actions[0]) - - @classmethod - def raise_if_cant_delete(cls, user, copr): - """ - Raise InsufficientRightsException if given copr cant be deleted - by given user. Return None otherwise. - """ - - if not user.admin and user != copr.owner: - raise exceptions.InsufficientRightsException( - "Only owners may delete their projects.") - - -class CoprPermissionsLogic(object): - - @classmethod - def get(cls, user, copr, searched_user): - query = (models.CoprPermission.query - .filter(models.CoprPermission.copr == copr) - .filter(models.CoprPermission.user == searched_user)) - - return query - - @classmethod - def get_for_copr(cls, user, copr): - query = models.CoprPermission.query.filter( - models.CoprPermission.copr == copr) - - return query - - @classmethod - def new(cls, user, copr_permission): - db.session.add(copr_permission) - - @classmethod - def update_permissions(cls, user, copr, copr_permission, - new_builder, new_admin): - - users_logic.UsersLogic.raise_if_cant_update_copr( - user, copr, "Only owners and admins may update" - " their projects permissions.") - - (models.CoprPermission.query - .filter(models.CoprPermission.copr_id == copr.id) - .filter(models.CoprPermission.user_id == copr_permission.user_id) - .update({"copr_builder": new_builder, - "copr_admin": new_admin})) - - @classmethod - def update_permissions_by_applier(cls, user, copr, copr_permission, new_builder, new_admin): - if copr_permission: - # preserve approved permissions if set - if (not new_builder or copr_permission.copr_builder != - helpers.PermissionEnum("approved")): - - copr_permission.copr_builder = new_builder - - if (not new_admin or copr_permission.copr_admin != - helpers.PermissionEnum("approved")): - - copr_permission.copr_admin = new_admin - else: - perm = models.CoprPermission( - user=user, - copr=copr, - copr_builder=new_builder, - copr_admin=new_admin) - - cls.new(user, perm) - - @classmethod - def delete(cls, user, copr_permission): - db.session.delete(copr_permission) - - -class CoprChrootsLogic(object): - - @classmethod - def mock_chroots_from_names(cls, user, names): - db_chroots = models.MockChroot.query.all() - mock_chroots = [] - for ch in db_chroots: - if ch.name in names: - mock_chroots.append(ch) - - return mock_chroots - - @classmethod - def new(cls, user, mock_chroot): - db.session.add(mock_chroot) - - @classmethod - def new_from_names(cls, user, copr, names): - for mock_chroot in cls.mock_chroots_from_names(user, names): - db.session.add( - models.CoprChroot(copr=copr, mock_chroot=mock_chroot)) - - @classmethod - def update_buildroot_pkgs(cls, copr, chroot, buildroot_pkgs): - copr_chroot = copr.check_copr_chroot(chroot) - if copr_chroot: - copr_chroot.buildroot_pkgs = buildroot_pkgs - db.session.add(copr_chroot) - - @classmethod - def update_from_names(cls, user, copr, names): - current_chroots = copr.mock_chroots - new_chroots = cls.mock_chroots_from_names(user, names) - # add non-existing - for mock_chroot in new_chroots: - if mock_chroot not in current_chroots: - db.session.add( - models.CoprChroot(copr=copr, mock_chroot=mock_chroot)) - - # delete no more present - to_remove = [] - for mock_chroot in current_chroots: - if mock_chroot not in new_chroots: - # can't delete here, it would change current_chroots and break - # iteration - to_remove.append(mock_chroot) - - for mc in to_remove: - copr.mock_chroots.remove(mc) - - -class MockChrootsLogic(object): - - @classmethod - def get(cls, user, os_release, os_version, arch, active_only=False): - return (models.MockChroot.query - .filter(models.MockChroot.os_release == os_release, - models.MockChroot.os_version == os_version, - models.MockChroot.arch == arch)) - - @classmethod - def get_from_name(cls, chroot_name, active_only=False): - """ - Return MockChroot object for textual representation of chroot - """ - - name_tuple = cls.tuple_from_name(None, chroot_name) - return cls.get(None, name_tuple[0], name_tuple[1], - name_tuple[2], active_only=active_only) - - @classmethod - def get_multiple(cls, user, active_only=False): - query = models.MockChroot.query - if active_only: - query = query.filter(models.MockChroot.is_active == True) - return query - - @classmethod - def add(cls, user, name): - name_tuple = cls.tuple_from_name(user, name) - if cls.get(user, *name_tuple).first(): - raise exceptions.DuplicateException( - "Mock chroot with this name already exists.") - new_chroot = models.MockChroot(os_release=name_tuple[0], - os_version=name_tuple[1], - arch=name_tuple[2]) - cls.new(user, new_chroot) - return new_chroot - - @classmethod - def new(cls, user, mock_chroot): - db.session.add(mock_chroot) - - @classmethod - def edit_by_name(cls, user, name, is_active): - name_tuple = cls.tuple_from_name(user, name) - mock_chroot = cls.get(user, *name_tuple).first() - if not mock_chroot: - raise exceptions.NotFoundException( - "Mock chroot with this name doesn't exist.") - - mock_chroot.is_active = is_active - cls.update(user, mock_chroot) - return mock_chroot - - @classmethod - def update(cls, user, mock_chroot): - db.session.add(mock_chroot) - - @classmethod - def delete_by_name(cls, user, name): - name_tuple = cls.tuple_from_name(user, name) - mock_chroot = cls.get(user, *name_tuple).first() - if not mock_chroot: - raise exceptions.NotFoundException( - "Mock chroot with this name doesn't exist.") - - cls.delete(user, mock_chroot) - - @classmethod - def delete(cls, user, mock_chroot): - db.session.delete(mock_chroot) - - @classmethod - def tuple_from_name(cls, user, name): - """ - valid name can be "fedora-rawhide-x86_64" or even "fedora-rawhide" - """ - - split_name = name.rsplit('-', 1) - if len(split_name) < 2: - raise exceptions.MalformedArgumentException( - "Chroot Name doesn't contain dash," - " can't determine chroot architecure.") - - if '-' in split_name[0]: - os_release, os_version = (split_name[0].rsplit('-'))[0:2] - else: - os_release, os_version = split_name[0], '' - - arch = split_name[1] - return (os_release, os_version, arch) diff --git a/coprs_frontend/coprs/logic/users_logic.py b/coprs_frontend/coprs/logic/users_logic.py deleted file mode 100644 index 4c2638b..0000000 --- a/coprs_frontend/coprs/logic/users_logic.py +++ /dev/null @@ -1,26 +0,0 @@ -from coprs import exceptions - - -class UsersLogic(object): - - @classmethod - def raise_if_cant_update_copr(cls, user, copr, message): - """ - Raise InsufficientRightsException if given user cant update - given copr. Return None otherwise. - """ - - # TODO: this is a bit inconsistent - shouldn't the user method be - # called can_update? - if not user.can_edit(copr): - raise exceptions.InsufficientRightsException(message) - - @classmethod - def raise_if_cant_build_in_copr(cls, user, copr, message): - """ - Raises InsufficientRightsException if given user cant build in - given copr. Return None otherwise. - """ - - if not user.can_build_in(copr): - raise exceptions.InsufficientRightsException(message) diff --git a/coprs_frontend/coprs/models.py b/coprs_frontend/coprs/models.py deleted file mode 100644 index b4ac6ed..0000000 --- a/coprs_frontend/coprs/models.py +++ /dev/null @@ -1,457 +0,0 @@ -import datetime - -from sqlalchemy.ext.associationproxy import association_proxy -from libravatar import libravatar_url - -from coprs import constants -from coprs import db -from coprs import helpers - - -class User(db.Model, helpers.Serializer): - - """ - Represents user of the copr frontend - """ - - id = db.Column(db.Integer, primary_key=True) - # openid_name for fas, e.g. http://bkabrda.id.fedoraproject.org/ - openid_name = db.Column(db.String(100), nullable=False) - # just mail :) - mail = db.Column(db.String(150), nullable=False) - # just timezone ;) - timezone = db.Column(db.String(50), nullable=True) - # is this user proven? proven users can modify builder memory and - # timeout for single builds - proven = db.Column(db.Boolean, default=False) - # is this user admin of the system? - admin = db.Column(db.Boolean, default=False) - # stuff for the cli interface - api_login = db.Column(db.String(40), nullable=False, default="abc") - api_token = db.Column(db.String(40), nullable=False, default="abc") - api_token_expiration = db.Column( - db.Date, nullable=False, default=datetime.date(2000, 1, 1)) - - @property - def name(self): - """ - Return the short username of the user, e.g. bkabrda - """ - - return self.openid_name.replace( - ".id.fedoraproject.org/", "").replace("http://", "") - - def permissions_for_copr(self, copr): - """ - Get permissions of this user for the given copr. - Caches the permission during one request, - so use this if you access them multiple times - """ - - if not hasattr(self, "_permissions_for_copr"): - self._permissions_for_copr = {} - if not copr.name in self._permissions_for_copr: - self._permissions_for_copr[copr.name] = (CoprPermission.query - .filter_by(user=self).filter_by(copr=copr).first()) - return self._permissions_for_copr[copr.name] - - def can_build_in(self, copr): - """ - Determine if this user can build in the given copr. - """ - - can_build = False - if copr.owner == self: - can_build = True - if (self.permissions_for_copr(copr) and - self.permissions_for_copr(copr).copr_builder == - helpers.PermissionEnum("approved")): - - can_build = True - - return can_build - - def can_edit(self, copr): - """ - Determine if this user can edit the given copr. - """ - - can_edit = False - if copr.owner == self: - can_edit = True - if (self.permissions_for_copr(copr) and - self.permissions_for_copr(copr).copr_admin == - helpers.PermissionEnum("approved")): - - can_edit = True - - return can_edit - - @classmethod - def openidize_name(cls, name): - """ - Create proper openid_name from short name. - - >>> user.openid_name == User.openidize_name(user.name) - True - """ - - return "http://{0}.id.fedoraproject.org/".format(name) - - @property - def serializable_attributes(self): - # enumerate here to prevent exposing credentials - return ["id", "name"] - - @property - def coprs_count(self): - """ - Get number of coprs for this user. - """ - - return (Copr.query.filter_by(owner=self). - filter_by(deleted=False). - count()) - - @property - def gravatar_url(self): - """ - Return url to libravatar image. - """ - - try: - return libravatar_url(email=self.mail) - except IOError: - return "" - - -class Copr(db.Model, helpers.Serializer): - - """ - Represents a single copr (private repo with builds, mock chroots, etc.). - """ - - id = db.Column(db.Integer, primary_key=True) - # name of the copr, no fancy chars (checked by forms) - name = db.Column(db.String(100), nullable=False) - # string containing urls of additional repos (separated by space) - # that this copr will pull dependencies from - repos = db.Column(db.Text) - # time of creation as returned by int(time.time()) - created_on = db.Column(db.Integer) - # description and instructions given by copr owner - description = db.Column(db.Text) - instructions = db.Column(db.Text) - deleted = db.Column(db.Boolean, default=False) - - # relations - owner_id = db.Column(db.Integer, db.ForeignKey("user.id")) - owner = db.relationship("User", backref=db.backref("coprs")) - mock_chroots = association_proxy("copr_chroots", "mock_chroot") - - __mapper_args__ = { - "order_by": created_on.desc() - } - - @property - def repos_list(self): - """ - Return repos of this copr as a list of strings - """ - return self.repos.split() - - @property - def active_chroots(self): - """ - Return list of active mock_chroots of this copr - """ - - return filter(lambda x: x.is_active, self.mock_chroots) - - @property - def build_count(self): - """ - Return number of builds in this copr - """ - - return len(self.builds) - - def check_copr_chroot(self, chroot): - """ - Return object of chroot, if is related to our copr or None - """ - - result = None - # there will be max ~10 chroots per build, iteration will be probably - # faster than sql query - for copr_chroot in self.copr_chroots: - if copr_chroot.mock_chroot_id == chroot.id: - result = copr_chroot - break - return result - - def buildroot_pkgs(self, chroot): - """ - Return packages in minimal buildroot for given chroot. - """ - - result = "" - # this is ugly as user can remove chroot after he submit build, but - # lets call this feature - copr_chroot = self.check_copr_chroot(chroot) - if copr_chroot: - result = copr_chroot.buildroot_pkgs - return result - - -class CoprPermission(db.Model, helpers.Serializer): - - """ - Association class for Copr<->Permission relation - """ - - # see helpers.PermissionEnum for possible values of the fields below - # can this user build in the copr? - copr_builder = db.Column(db.SmallInteger, default=0) - # can this user serve as an admin? (-> edit and approve permissions) - copr_admin = db.Column(db.SmallInteger, default=0) - - # relations - user_id = db.Column(db.Integer, db.ForeignKey("user.id"), primary_key=True) - user = db.relationship("User", backref=db.backref("copr_permissions")) - copr_id = db.Column(db.Integer, db.ForeignKey("copr.id"), primary_key=True) - copr = db.relationship("Copr", backref=db.backref("copr_permissions")) - - -class Build(db.Model, helpers.Serializer): - - """ - Representation of one build in one copr - """ - - id = db.Column(db.Integer, primary_key=True) - # list of space separated urls of packages to build - pkgs = db.Column(db.Text) - # was this build canceled by user? - canceled = db.Column(db.Boolean, default=False) - # list of space separated additional repos - repos = db.Column(db.Text) - # the three below represent time of important events for this build - # as returned by int(time.time()) - submitted_on = db.Column(db.Integer, nullable=False) - started_on = db.Column(db.Integer) - ended_on = db.Column(db.Integer) - # url of the build results - results = db.Column(db.Text) - # memory requirements for backend builder - memory_reqs = db.Column(db.Integer, default=constants.DEFAULT_BUILD_MEMORY) - # maximum allowed time of build, build will fail if exceeded - timeout = db.Column(db.Integer, default=constants.DEFAULT_BUILD_TIMEOUT) - - # relations - user_id = db.Column(db.Integer, db.ForeignKey("user.id")) - user = db.relationship("User", backref=db.backref("builds")) - copr_id = db.Column(db.Integer, db.ForeignKey("copr.id")) - copr = db.relationship("Copr", backref=db.backref("builds")) - - chroots = association_proxy("build_chroots", "mock_chroot") - - @property - def chroot_states(self): - return map(lambda chroot: chroot.status, self.build_chroots) - - @property - def has_pending_chroot(self): - return helpers.StatusEnum("pending") in self.chroot_states - - @property - def status(self): - """ - Return build status according to build status of its chroots - """ - - if self.canceled: - return helpers.StatusEnum("canceled") - - for state in ["failed", "running", "pending", "succeeded"]: - if helpers.StatusEnum(state) in self.chroot_states: - return helpers.StatusEnum(state) - - @property - def state(self): - """ - Return text representation of status of this build - """ - - if self.status is not None: - return helpers.StatusEnum(self.status) - - return "unknown" - - @property - def cancelable(self): - """ - Find out if this build is cancelable. - - ATM, build is cancelable only if it wasn"t grabbed by backend. - """ - - return self.status == helpers.StatusEnum("pending") - - -class MockChroot(db.Model, helpers.Serializer): - - """ - Representation of mock chroot - """ - - id = db.Column(db.Integer, primary_key=True) - # fedora/epel/..., mandatory - os_release = db.Column(db.String(50), nullable=False) - # 18/rawhide/..., optional (mock chroot doesn"t need to have this) - os_version = db.Column(db.String(50), nullable=False) - # x86_64/i686/..., mandatory - arch = db.Column(db.String(50), nullable=False) - is_active = db.Column(db.Boolean, default=True) - - @property - def name(self): - """ - Textual representation of name of this chroot - """ - - if self.os_version: - format_string = "{rel}-{ver}-{arch}" - else: - format_string = "{rel}-{arch}" - return format_string.format(rel=self.os_release, - ver=self.os_version, - arch=self.arch) - - -class CoprChroot(db.Model, helpers.Serializer): - - """ - Representation of Copr<->MockChroot relation - """ - - buildroot_pkgs = db.Column(db.Text) - mock_chroot_id = db.Column( - db.Integer, db.ForeignKey("mock_chroot.id"), primary_key=True) - mock_chroot = db.relationship( - "MockChroot", backref=db.backref("copr_chroots")) - copr_id = db.Column(db.Integer, db.ForeignKey("copr.id"), primary_key=True) - copr = db.relationship("Copr", - backref=db.backref( - "copr_chroots", - single_parent=True, - cascade="all,delete,delete-orphan")) - - -class BuildChroot(db.Model, helpers.Serializer): - - """ - Representation of Build<->MockChroot relation - """ - - mock_chroot_id = db.Column(db.Integer, db.ForeignKey("mock_chroot.id"), - primary_key=True) - mock_chroot = db.relationship("MockChroot", backref=db.backref("builds")) - build_id = db.Column(db.Integer, db.ForeignKey("build.id"), - primary_key=True) - build = db.relationship("Build", backref=db.backref("build_chroots")) - status = db.Column(db.Integer, default=helpers.StatusEnum("pending")) - - @property - def name(self): - """ - Textual representation of name of this chroot - """ - - return self.mock_chroot.name - - @property - def state(self): - """ - Return text representation of status of this build chroot - """ - - if self.status is not None: - return helpers.StatusEnum(self.status) - - return "unknown" - - -class LegalFlag(db.Model, helpers.Serializer): - id = db.Column(db.Integer, primary_key=True) - # message from user who raised the flag (what he thinks is wrong) - raise_message = db.Column(db.Text) - # time of raising the flag as returned by int(time.time()) - raised_on = db.Column(db.Integer) - # time of resolving the flag by admin as returned by int(time.time()) - resolved_on = db.Column(db.Integer) - - # relations - copr_id = db.Column(db.Integer, db.ForeignKey("copr.id"), nullable=True) - # cascade="all" means that we want to keep these even if copr is deleted - copr = db.relationship( - "Copr", backref=db.backref("legal_flags", cascade="all")) - # user who reported the problem - reporter_id = db.Column(db.Integer, db.ForeignKey("user.id")) - reporter = db.relationship("User", - backref=db.backref("legal_flags_raised"), - foreign_keys=[reporter_id], - primaryjoin="LegalFlag.reporter_id==User.id") - # admin who resolved the problem - resolver_id = db.Column( - db.Integer, db.ForeignKey("user.id"), nullable=True) - resolver = db.relationship("User", - backref=db.backref("legal_flags_resolved"), - foreign_keys=[resolver_id], - primaryjoin="LegalFlag.resolver_id==User.id") - - -class Action(db.Model, helpers.Serializer): - - """ - Representation of a custom action that needs - backends cooperation/admin attention/... - """ - - id = db.Column(db.Integer, primary_key=True) - # delete, rename, ...; see helpers.ActionTypeEnum - action_type = db.Column(db.Integer, nullable=False) - # copr, ...; downcase name of class of modified object - object_type = db.Column(db.String(20)) - # id of the modified object - object_id = db.Column(db.Integer) - # old and new values of the changed property - old_value = db.Column(db.String(255)) - new_value = db.Column(db.String(255)) - # additional data - data = db.Column(db.Text) - # result of the action, see helpers.BackendResultEnum - result = db.Column( - db.Integer, default=helpers.BackendResultEnum("waiting")) - # optional message from the backend/whatever - message = db.Column(db.Text) - # time created as returned by int(time.time()) - created_on = db.Column(db.Integer) - # time ended as returned by int(time.time()) - ended_on = db.Column(db.Integer) - - def __str__(self): - return self.__unicode__() - - def __unicode__(self): - if self.action_type == helpers.ActionTypeEnum("delete"): - return "Deleting {0} {1}".format(self.object_type, self.old_value) - elif self.action_type == helpers.ActionTypeEnum("rename"): - return "Renaming {0} from {1} to {2}.".format(self.object_type, - self.old_value, - self.new_value) - elif self.action_type == helpers.ActionTypeEnum("legal-flag"): - return "Legal flag on copr {0}.".format(self.old_value) - - return "Action {0} on {1}, old value: {2}, new value: {3}.".format( - self.action_type, self.object_type, self.old_value, self.new_value) diff --git a/coprs_frontend/coprs/signals.py b/coprs_frontend/coprs/signals.py deleted file mode 100644 index fde098a..0000000 --- a/coprs_frontend/coprs/signals.py +++ /dev/null @@ -1,6 +0,0 @@ -import blinker - -coprs_signals = blinker.Namespace() - -build_finished = coprs_signals.signal("build-finished") -copr_created = coprs_signals.signal("copr-created") diff --git a/coprs_frontend/coprs/static/README b/coprs_frontend/coprs/static/README deleted file mode 100644 index 7ba0cef..0000000 --- a/coprs_frontend/coprs/static/README +++ /dev/null @@ -1,12 +0,0 @@ -Public, static content goes here. Users can create rewrite rules to link to -content in the static dir. For example, django commonly uses /media/ -directories for static content. For example in a .htaccess file in a -wsgi/.htaccess location, developers could put: - -RewriteEngine On -RewriteRule ^application/media/(.+)$ /static/media/$1 [L] - -Then copy the media/* content to yourapp/wsgi/static/media/ and it should -just work. - -Note: The ^application/ part of the URI match is required. diff --git a/coprs_frontend/coprs/static/copr.css b/coprs_frontend/coprs/static/copr.css deleted file mode 100644 index cb6621c..0000000 --- a/coprs_frontend/coprs/static/copr.css +++ /dev/null @@ -1,406 +0,0 @@ -html, body { - font-family: Cantarell, "Droid Sans", Verdana, sans-serif; - font-size: 1em; - - color: #000; - margin: 0px; - padding: 0px; -} - -a { - color: #3d69a8; - text-decoration: none; -} - -h1 { - font-weight: normal; - color: #3d69a8; -} - -h2 { - font-size: 1.1em; -} - -#logo { - position: relative; - top: 8px; -} - -div.menu { - font-size: 0.9em; - background-image: url("header_background.png"); - background-repeat: repeat-x; - height: 81px; - margin-bottom: 3em; -} - -div.flash { - background-color: #f9f9f9; - font-size: 1.2em; - text-align: center; - padding: 0.5em; - margin-bottom: 1em; - - border-radius: 5px; - -moz-border-radius: 5px; - -webkit-border-radius: 5px; -} - -div.pagination { - width: 100%; - text-align: center; - font-size: 1.3em; - margin: 1em; -} - -div.login, div.login a { - color: white; - font-weight: bold; - text-decoration: none; - line-height: 250%; - text-align: right; - - position: relative; - margin-left: 0.3em; -} - -div.login { - float: right; -} - -div.login .text { - font-weight: normal; -} - -div.page, div.menu-inner { - width: 780px; - margin-left: auto; - margin-right: auto; -} - -div.user-info { - width: 174px; - margin-right: 25px; - float: left; - border-right: 1px solid #c3c3c3; - - color: #565656; - font-size: 1.5em; - font-weight: bold; -} - -div.user-info .coprs-count { - margin: 0px; - font-size: 0.8em; -} - -div.user-info .other-text { - margin: 0px; - font-size: 0.6em; -} - -div.about-copr { - width: 100%; - background-color: #ececec; - padding-top: 1em; - padding-bottom: 1em; - - border-radius: 5px; - -moz-border-radius: 5px; - -webkit-border-radius: 5px; -} - -div.about-copr p { - padding-left: 1em; - padding-right: 1em; - margin-top: 0.3em; - margin-bottom: 0.3em; -} - -div.coprs-list-thin, div.coprs-list-thick { - float: left; -} - -div.coprs-list-thin { - width: 580px; -} - -div.coprs-list-thick { - width: 100%; -} - -div.copr { - width: 100%; - background-color: #f9f9f9; - padding-top: 1em; - padding-bottom: 1em; - - border-radius: 5px; - -moz-border-radius: 5px; - -webkit-border-radius: 5px; -} - -a.coprs-list { - padding-left: 0.71em; - padding-right: 0.71em; - font-size: 1.4em; - font-weight: bold; - display: block; -} - -div.copr p { - padding-left: 1em; - padding-right: 1em; - margin-top: 0.3em; - margin-bottom: 0.3em; -} - -div.copr .repos { - color: #808080; -} - -div.search-results { - font-size: 1.3em; - text-align: center; -} - -div.add-copr { - background-color: #ececec; - padding: 0.5em; - margin-bottom: 0.6em; - - color: #cccccc; - text-align: center; - vertical-align: middle; - font-size: 1.1em; - font-weight: bold; - - border-radius: 5px; - -moz-border-radius: 5px; - -webkit-border-radius: 5px; -} - -div.add-copr a { - text-decoration: none; - color: #666666; -} - -div.horizontal-menu { - background-color: #ededed; - height: 3em; - - border-radius: 10px; - -moz-border-radius: 10px; - -webkit-border-radius: 10px; -} - -div.horizontal-menu a { - display: block; - padding: 0.6em; - font-size: 1.1em; - font-weight: bold; - color: #4d4d4d; -} - -div.horizontal-menu ul { - margin: 0px; - padding: 0px; - float: left; -} - -div.horizontal-menu li { - display: inline; - float: left; -} - -div.horizontal-menu li.selected a, div.horizontal-menu li.left-for-now a { - color: #db3279; - margin-left: auto; - margin-right: auto; -} - -div.horizontal-menu li.selected a, div.horizontal-menu li.hovered a { - background: url("pink_arrow.png") no-repeat; - background-position: center bottom; -} - -div.pkg-url-list { - white-space: pre-wrap; - background-color: #f9f9f9; - font-family: monospace; - padding: 1em; - line-height: 120%; - font-size: 1.1em; -} - -div.shift-right { - margin-left: 1em; -} - -dt.field-label { - margin: 15px 0; - font-weight: bold; -} - -input.rounded { - padding-left: 4px; - border-radius: 10px; - -moz-border-radius: 10px; - -webkit-border-radius: 10px; -} - -input.fulltext-submit { - color: white; - font-weight: bold; - background-color: #3d69a8; - border-radius: 10px; - -moz-border-radius: 10px; - -webkit-border-radius: 10px; -} - -p.form-error { - color: red; -} - -table.releases { - width: 100%; - border-collapse:collapse; -} - -table.chroots-set { - display: inline; - margin-left: 40px; -} - -table.builds-table { - width: 100%; -} - -table.builds-table form { - display: inline; -} - -table.builds-table tr.details { - width: 100%; - display: none; -} - -tr.build-state:hover { - text-decoration: underline; - background-color: #E6E6E6; -} - -.build-pending { - color: #3B6EB4; -} - -.build-running { - color: #FF6600; -} - -.build-succeeded { - color: #22DD22; -} - -.build-failed { - color: #DD2222; -} - -.build-canceled { - color: #CDC90C; -} - -table.releases th { - background-color: #f2f2f2; - text-align: left; -} - -table.releases th.leftmost { - border-top-left-radius: 10px; - -moz-border-radius-topleft: 10px; - -webkit-border-top-left-radius: 10px; - border-bottom-left-radius: 10px; - -moz-border-radius-bottomleft: 10px; - -webkit-border-bottom-left-radius: 10px; -} - -table.releases th.rightmost { - border-top-right-radius: 10px; - -moz-border-radius-topright: 10px; - -webkit-border-top-right-radius: 10px; - border-bottom-right-radius: 10px; - -moz-border-radius-bottomright: 10px; - -webkit-border-bottom-right-radius: 10px; -} - -table.releases tr.release-end { - border-bottom: 3px solid #f2f2f2; -} - -table.monitor { - margin: 0 auto; - width: 90%; -} - -form.legal-flag, form.legal-flag input { - color: #888888; - margin-top: 5px; -} - -div.legal-flag { - margin: 10px; - font-size: 1.2em; -} - -div.legal-flag div.message { - display: none; - margin-bottom: 10px; - font-size: 1em; -} - -div.legal-flag form { - display: inline; - text-align: right; - float: right; -} - -hr { - margin-top: 25px; - margin-bottom: 25px; -} - - -.footer { - padding: 25px; - background: white; - font-size: 0.8em; -} - -.footer p { - margin: 0 auto; - width: 300px; -} - -.footer a { - margin-right: 10px; - padding-right: 10px; - border-right: 1px solid #444; -} - -.footer .last { - border-right: none; -} - -.required:before { - content: "* "; - color: red; - font-weight: bold; -} - -textarea { - width: 100%; -} - -.centered { - text-align: center; -} diff --git a/coprs_frontend/coprs/static/copr.js b/coprs_frontend/coprs/static/copr.js deleted file mode 100644 index 41b9fd6..0000000 --- a/coprs_frontend/coprs/static/copr.js +++ /dev/null @@ -1,29 +0,0 @@ -// showing build details -$(document).ready(function () { - $("table.builds-table tr[class^='build-']").each(function (i, e) { - $(this).click(function() { $("table.builds-table tr.details").hide(); $(this).next().show(); }); - }); -}); - -// build detail menu arrow slider -$(document).ready(function() { - $("div.horizontal-menu li").click( - function() { - $("div.horizontal-menu li.selected").removeClass('selected').addClass('left-for-now'); - $(this).toggleClass('clicked'); - }, - function() { - $("div.horizontal-menu li.left-for-now").removeClass('left-for-now').addClass('selected'); - $(this).toggleClass('clicked'); - } - ); -}); - -// admin legal-flag divs rolling -$(document).ready(function() { - $("div.legal-flag").mouseenter( - function() { - $(this).children(".message").show("fast"); - } - ); -}); diff --git a/coprs_frontend/coprs/static/copr_logo.png b/coprs_frontend/coprs/static/copr_logo.png deleted file mode 100644 index 4576f78..0000000 Binary files a/coprs_frontend/coprs/static/copr_logo.png and /dev/null differ diff --git a/coprs_frontend/coprs/static/default_user.png b/coprs_frontend/coprs/static/default_user.png deleted file mode 100644 index 31b6c60..0000000 Binary files a/coprs_frontend/coprs/static/default_user.png and /dev/null differ diff --git a/coprs_frontend/coprs/static/favicon.ico b/coprs_frontend/coprs/static/favicon.ico deleted file mode 100644 index 79d0ba9..0000000 Binary files a/coprs_frontend/coprs/static/favicon.ico and /dev/null differ diff --git a/coprs_frontend/coprs/static/header_background.png b/coprs_frontend/coprs/static/header_background.png deleted file mode 100644 index 61fcb6f..0000000 Binary files a/coprs_frontend/coprs/static/header_background.png and /dev/null differ diff --git a/coprs_frontend/coprs/static/pink_arrow.png b/coprs_frontend/coprs/static/pink_arrow.png deleted file mode 100644 index fec3cf7..0000000 Binary files a/coprs_frontend/coprs/static/pink_arrow.png and /dev/null differ diff --git a/coprs_frontend/coprs/templates/404.html b/coprs_frontend/coprs/templates/404.html deleted file mode 100644 index bfd8bfd..0000000 --- a/coprs_frontend/coprs/templates/404.html +++ /dev/null @@ -1,10 +0,0 @@ -{% extends "layout.html" %} -{% block title %}Not Found{% endblock %} -{% block header %}Not Found!{% endblock %} -{% block body %} - {% if message %} - {{ message }} - {% else %} - The thing you're looking for does not exist. - {% endif %} -{% endblock %} diff --git a/coprs_frontend/coprs/templates/_helpers.html b/coprs_frontend/coprs/templates/_helpers.html deleted file mode 100644 index 04630bb..0000000 --- a/coprs_frontend/coprs/templates/_helpers.html +++ /dev/null @@ -1,33 +0,0 @@ -{% macro render_field(field, label=None, class='') %} - {% if not kwargs['hidden'] %} -
{{ label or field.label }}
-
- {% if field.errors %} - {% for error in field.errors %} -

{{ error }}

- {% endfor %} - {% endif %} - {{ field(**kwargs)|safe }} -
- {% else %} - {{ field(**kwargs)|safe }} - {% endif %} -{% endmacro %} - -{% macro render_pagination(request, paginator) %} - {% if paginator.pages > 1 %} - {% if paginator.border_url(request, True) %} - {{ paginator.border_url(request, True)[1] }} ... - {% endif %} - {% for page in paginator.get_urls(request) %} - {% if page[1] != paginator.page %} {# no url for current page #} - {{ page[1] }} - {% else %} - {{ page[1] }} - {% endif %} - {% endfor %} - {% if paginator.border_url(request, False) %} - ... {{ paginator.border_url(request, False)[1] }} - {% endif %} - {% endif %} -{% endmacro %} diff --git a/coprs_frontend/coprs/templates/admin/index.html b/coprs_frontend/coprs/templates/admin/index.html deleted file mode 100644 index 25562a2..0000000 --- a/coprs_frontend/coprs/templates/admin/index.html +++ /dev/null @@ -1,5 +0,0 @@ -{% extends "admin/layout.html" %} - -{% block admin_body %} -Admin body -{% endblock %} diff --git a/coprs_frontend/coprs/templates/admin/layout.html b/coprs_frontend/coprs/templates/admin/layout.html deleted file mode 100644 index c8d48c9..0000000 --- a/coprs_frontend/coprs/templates/admin/layout.html +++ /dev/null @@ -1,14 +0,0 @@ -{% extends "layout.html" %} - -{% block title %}Coprs - Admin{% endblock %} - -{% block body %} -
- -
-{% block admin_body %}{% endblock %} -{% endblock %} diff --git a/coprs_frontend/coprs/templates/admin/legal-flag.html b/coprs_frontend/coprs/templates/admin/legal-flag.html deleted file mode 100644 index 75f61c1..0000000 --- a/coprs_frontend/coprs/templates/admin/legal-flag.html +++ /dev/null @@ -1,34 +0,0 @@ -{% extends "admin/layout.html" %} - -{% block legal_flag_selected %}selected{% endblock %} - -{% block admin_body %} - {% for flag in legal_flags %} - - {% else %} -

No coprs marked for legal review

- {% endfor %} -{% endblock %} diff --git a/coprs_frontend/coprs/templates/api.html b/coprs_frontend/coprs/templates/api.html deleted file mode 100644 index 9b511f3..0000000 --- a/coprs_frontend/coprs/templates/api.html +++ /dev/null @@ -1,448 +0,0 @@ -{% extends "layout.html" %} -{% block title %}API for Copr{% endblock %} -{% block header %}API for the Copr Build System{% endblock %} -{% block body %} - {% if error %}

Error: {{ error }}

{% endif %} - -
-

Copr API

- -

API Token

-

In order to access the API, you will need to provide an API token. - This token is unique, specific to you and - should not be shared!. -

- -

The API token is valid for {{ config['API_TOKEN_EXPIRATION'] }} days after it has been generated. -

- - {% if g.user %} -

Your information (you can directly paste this into ~/.config/copr):

-
-[copr-cli]
-login = {{ g.user.api_login }}
-username = {{ g.user.name }}
-token = {{ g.user.api_token }}
-copr_url = http://copr.fedoraproject.org
-# expiration date: {{ g.user.api_token_expiration }}
-
- - - - - {% else %} -

You need to be logged in to see your API token.

- {% endif %} - -

The API

- -

To make an API call to Copr, make a request to URL corresponding to - given call (URLs are listed below). Parameters are denoted by angle - brackets. Result is represented as JSON map with "output": "ok" key-value - pair on success or "output": "notok" on failure. The rest of the map - represents result of the call and is described below for individual - calls.

- -

List someone's projects

- -

URL:

-
/api/coprs/<username>/
-
or
-
/api/coprs/?username="<username>"
- -

URL parameters:

- - -

Result:

- - -

Example call URL

-
https://copr.fedoraproject.org/api/coprs/jdaniels/
- -

Example results

-
-    {
-      "output": "ok",
-      "repos": [
-        {
-          "yum_repos": {
-            "fedora-19-i686": "https://copr-be.cloud.fedoraproject.org/results/jdaniels/log4j/fedora-19-i686/",
-            "fedora-19-x86_64": "https://copr-be.cloud.fedoraproject.org/results/jdaniels/log4j/fedora-19-x86_64/"
-          },
-          "additional_repos": "",
-          "instructions": "",
-          "name": "log4j",
-          "description": "Java logging package"
-        }
-      ]
-    }
-    
- -

Detail of project

- -

URL:

-
/api/coprs/<username>/<projectname>/detail/
- -

URL parameters:

- - -

Result:

- - -

Example call URL

-
https://copr.fedoraproject.org/api/coprs/jdaniels/log4j/detail/
- -

Example results

-
-    {
-      "output": "ok",
-      "repos": [
-        {
-          "yum_repos": {
-            "fedora-19-i686": "https://copr-be.cloud.fedoraproject.org/results/jdaniels/log4j/fedora-19-i686/",
-            "fedora-19-x86_64": "https://copr-be.cloud.fedoraproject.org/results/jdaniels/log4j/fedora-19-x86_64/"
-          },
-          "additional_repos": "",
-          "instructions": "",
-          "name": "log4j",
-          "description": "Java logging package",
-          "last_modified": 1386695673 
-        }
-      ]
-    }
-    
- -

Create new project

- -

Login required

- -

URL:

-
/api/coprs/<username>/new/
- -

URL parameters:

- - -

Parameters sent by POST:

- - -

Add new build

- -

Login required

- -

URL:

-
/api/coprs/<username>/<projectname>/new_build/
- -

URL parameters:

- - -

Parameters sent by POST:

- - -

Example results

-
-    {
-      "output": "ok",
-      "message": "Build was added to log4j.",
-      "id": 5
-    }
-    
- -

Query build status

- -

Login required

- -

URL:

-
/api/coprs/build_status/<build_id>/
- -

URL parameters:

- - -

Result

- - -

Example result

-
-    {
-      "status": "pending",
-      "output": "ok"
-    }
-    
- -

Query build detail

- -

URL:

-
/api/coprs/build_detail/<build_id>/
- -

URL parameters:

- - -

Result

- - -

Example result

-
-    {
-      "status": "pending",
-      "owner": "msuchy",
-      "project": "myproject",
-      "output": "ok"
-    }
-    
- -

Cancel build

- -

Login required

- -

URL:

-
/api/coprs/cancel_build/<build_id>/
- -

URL parameters:

- - -

Result

- - -

Example result

-
-    {
-      "status": "Build canceled",
-      "output": "ok"
-    }
-    
- -

Copr Modification

- -

Login required

- -

URL:

-
/api/coprs/<username>/<coprname>/modify/
- -

URL parameters:

- - -

Parameters sent by POST:

- - -

Result

- - -

Example result

-
-    {
-      "output": "ok",
-      "repos": "foo",
-      "description": "bar",
-      "instructions": "baz"
-    }
-    
- -

Chroot Modification

- -

Login required

- -

URL:

-
/api/coprs/<username>/<coprname>/modify/<chrootname>/
- -

URL parameters:

- - -

Parameters sent by POST:

- - -

Result

- - -

Example result

-
-    {
-      "output": "ok",
-      "buildroot_pkgs": "scl-utils-build"
-    }
-    
- -

Chroot details

- -

URL:

-
/api/coprs/<username>/<coprname>/detail/<chrootname>/
- -

URL parameters:

- - -

Result

- - -

Example result

-
-    {
-      "output": "ok",
-      "buildroot_pkgs": "scl-utils-build"
-    }
-    
- -

Search for project

- -

URL:

-
/api/coprs/search/<project>/
-
or
-
/api/coprs/?search="<project>"
- -

URL parameters:

- - -

Result:

- - -

Example call URL

-
https://copr.fedoraproject.org/api/coprs/search/tests/
- -

Example results

-
-    {
-      "output": "ok",
-      "repos": [
-        {
-          "username": "ignatenkobrain",
-          "coprname": "test",
-          "description": "Tests"
-        },
-          "username": "ignatenkobrain",
-          "coprname": "tests",
-          "description": ""
-        },
-        {
-          "username": "msuchy",
-          "coprname": "tests",
-          "description": "Copr testing repository, just for test various builds."
-        }
-      ]
-    }
-    
- -
-{% endblock %} diff --git a/coprs_frontend/coprs/templates/coprs/_coprs_forms.html b/coprs_frontend/coprs/templates/coprs/_coprs_forms.html deleted file mode 100644 index 6abe22f..0000000 --- a/coprs_frontend/coprs/templates/coprs/_coprs_forms.html +++ /dev/null @@ -1,114 +0,0 @@ -{% from "_helpers.html" import render_field %} - -{% macro copr_form(form, view, copr = None, username = None) %} - {# if using for updating, we need to pass name to url_for, but otherwise we need to pass nothing #} -
-
- {{ form.csrf_token }} - {{ render_field(form.id, hidden = True) }} - {% if copr is none %} - {{ render_field(form.name, label='Project Name', required = True, class="required") }} - {% else %} - {{ render_field(form.name, hidden = True) }} - {{ render_field(form.name, label='Project Name', disabled = True) }} - {% endif %} - {{ render_field(form.description, rows=5, cols=50, placeholder='Optional - describe your project briefly.') }} - {{ render_field(form.instructions, rows=5, cols=50, placeholder='Optional - describe how your project can be installed. Link to wiki is good as well.') }} -
You can use markdown syntax, inline HTML is forbidden..
-
Chroots
- {% if form._mock_chroots_error %} -

{{ form._mock_chroots_error }}

- {% endif %} - {% for group_set, chs in form.chroots_sets.items() %} - - {% for ch in chs %} - - - - {% endfor %} -
- {{ form|attr(ch)|attr('label') }} - {% if form|attr(ch)|attr('label') %} - {% else %} - {{ form|attr(ch)|attr('label') }} - {% endif %} - {{ form|attr(ch) }} - {% if copr and form|attr(ch)|attr('data') %} - [Edit] - {% endif %} -
- {% endfor %} - {{ render_field(form.repos, rows=5, cols=50, placeholder='Optional - URL to additional yum repos, which can be used during build. Space separated.') }} - {% if copr is none %}{# we're creating the copr, so display initial builds area #} - {{ render_field(form.initial_pkgs, rows=5, cols=50, placeholder='Optional - list of src.rpm to build initially. Can be skipped and submitted later.') }} - {% endif %} -
-
-
-{% endmacro %} - -{% macro copr_delete_form(form, copr) %} -
-
- {{ form.csrf_token }} -
- {% if form.verify.errors %} - {% for error in form.verify.errors %} -

{{ error }}

- {% endfor %} - {% endif %} -
- {{ form.verify }} -
-
-
-{% endmacro %} - -{% macro copr_permissions_form(form, copr, permissions) %} - {% if permissions %} -
- {{ form.csrf_token }} - - - {% for perm in permissions %} - - - - - - {% endfor %} -
UsernameIs BuilderIs Admin
{{ perm.user.name }} - {{ perm.copr_builder|perm_type_from_num }} - {% if perm.copr_builder != 0 %} - {{ form['copr_builder_{0}'.format(perm.user.id)] }} - {% endif %} - - {{ perm.copr_admin|perm_type_from_num }} - {% if perm.copr_admin != 0 %} - {{ form['copr_admin_{0}'.format(perm.user.id)] }} - {% endif %} -
-
-
- {% endif %} - {% endmacro %} - -{% macro copr_legal_flag_form(form, copr) %} - -{% endmacro %} diff --git a/coprs_frontend/coprs/templates/coprs/add.html b/coprs_frontend/coprs/templates/coprs/add.html deleted file mode 100644 index 21d901e..0000000 --- a/coprs_frontend/coprs/templates/coprs/add.html +++ /dev/null @@ -1,15 +0,0 @@ -{% extends "layout.html" %} -{% block title %}Add a Project{% endblock %} -{% block header %}Add a new Project{% endblock %} -{% from "coprs/_coprs_forms.html" import copr_form %} - -{% block body %} - -

Add a new Project

- -

-You agree to build only allowed content in Copr. Check if your license is allowed. -

- {{ copr_form(form, view = 'coprs_ns.copr_new', username=g.user.name) }} - -{% endblock %} diff --git a/coprs_frontend/coprs/templates/coprs/copr.repo b/coprs_frontend/coprs/templates/coprs/copr.repo deleted file mode 100644 index 1aaf318..0000000 --- a/coprs_frontend/coprs/templates/coprs/copr.repo +++ /dev/null @@ -1,6 +0,0 @@ -[{{ copr.owner.name }}-{{ copr.name }}] -name=Copr repo for {{ copr.name }} owned by {{ copr.owner.name }} -baseurl={{ url }} -skip_if_unavailable=True -gpgcheck=0 -enabled=1 diff --git a/coprs_frontend/coprs/templates/coprs/detail.html b/coprs_frontend/coprs/templates/coprs/detail.html deleted file mode 100644 index cc4535c..0000000 --- a/coprs_frontend/coprs/templates/coprs/detail.html +++ /dev/null @@ -1,41 +0,0 @@ -{% extends "layout.html" %} -{% block title %}{{ copr.owner.name }}/{{ copr.name }} Copr{% endblock %} - -{% block body %} -

- {{ copr.owner.name }} / - {{ copr.name }} -

-
- -
- {% block detail_body %}{% endblock %} -{% endblock %} diff --git a/coprs_frontend/coprs/templates/coprs/detail/_builds_forms.html b/coprs_frontend/coprs/templates/coprs/detail/_builds_forms.html deleted file mode 100644 index f6b7bd2..0000000 --- a/coprs_frontend/coprs/templates/coprs/detail/_builds_forms.html +++ /dev/null @@ -1,51 +0,0 @@ -{% from "_helpers.html" import render_field %} - -{% macro copr_build_form(form, view, copr) %} -
-
- {{ form.csrf_token }} - {{ render_field(form.pkgs, label='URLs of packages to build', rows = 10, cols = 50) }} - {% if g.user.proven %} - {{ render_field(form.memory_reqs) }} - {{ render_field(form.timeout) }} - {% else %} {# once we pass the hidden attribute, the field will just be hidden, it seems #} - {{ render_field(form.memory_reqs, hidden = True) }} - {{ render_field(form.timeout, hidden = True) }} - {% endif %} -
-

- You agree to build only allowed content in Copr. - Check if your license is allowed. -

-
-
-
-
-{% endmacro %} - -{% macro copr_build_cancel_form(build, page) %} - {% if build.cancelable %} -
- - -
- {% endif %} -{% endmacro %} - -{% macro copr_build_repeat_form(build, page) %} - {% if build.cancelable %} -
- - -
- {% endif %} -{% endmacro %} - -{% macro copr_build_delete_form(build, page) %} - {% if build.ended_on %} -
- - -
- {% endif %} -{% endmacro %} diff --git a/coprs_frontend/coprs/templates/coprs/detail/_builds_table.html b/coprs_frontend/coprs/templates/coprs/detail/_builds_table.html deleted file mode 100644 index 720ef76..0000000 --- a/coprs_frontend/coprs/templates/coprs/detail/_builds_table.html +++ /dev/null @@ -1,62 +0,0 @@ -{% from "coprs/detail/_builds_forms.html" import copr_build_cancel_form, copr_build_repeat_form, copr_build_delete_form %} - -{% macro builds_table(builds, page) %} - {% if builds %} - - - - - - - - - - {% for build in builds %} - - -{% if g.user %} - -{% else %} - -{% endif %} - - -{% if g.user %} - - -{% else %} - - -{% endif %} - - - - - - {% endfor %} -
IdSubmitted onSubmitted byStarted onEnded onState
{{ build.id }}{{ build.submitted_on|localized_time(g.user.timezone) }}{{ build.submitted_on|localized_time("UTC") }}{{ build.user.name }}{{ build.started_on|localized_time(g.user.timezone) }}{{ build.ended_on|localized_time(g.user.timezone) }}{{ build.started_on|localized_time("UTC") }}{{ build.ended_on|localized_time("UTC") }}{{ build.state }}
-
- {% if g.user and g.user.can_build_in(copr) %} - {{ copr_build_cancel_form(build, page) }} - {% endif %} - {% if g.user and g.user.can_build_in(copr) %} - {{ copr_build_repeat_form(build, page) }} - {% endif %} - {% if g.user and g.user.can_edit(copr) %} - {{ copr_build_delete_form(build, page) }} - {% endif %} -
- {% if build.results %} -

Results:

{{ build.results }} - {% else %} -

No results yet.

- {% endif %} -
-

Package URLs:

-
{% if build.pkgs is not none %}{% for pkg in build.pkgs.split() %}{{ pkg }} -{% endfor %}{% endif %}
-
- {% else %} -

No builds so far

- {% endif %} -{% endmacro %} diff --git a/coprs_frontend/coprs/templates/coprs/detail/_permissions_table.html b/coprs_frontend/coprs/templates/coprs/detail/_permissions_table.html deleted file mode 100644 index d8ded5d..0000000 --- a/coprs_frontend/coprs/templates/coprs/detail/_permissions_table.html +++ /dev/null @@ -1,90 +0,0 @@ -{% macro permissions_table(permissions, current_user_permissions, copr, permissions_applier_form, permissions_form) %} - {% if permissions or g.user != copr.owner %} {# display the whole table if there are permissions or user can ask for them #} - {% if permissions_applier_form and g.user %} -
- {{ permissions_applier_form.csrf_token }} - {% endif %} - {% if permissions_form and g.user %} - - {{ permissions_form.csrf_token }} - {% endif %} - - - {% for perm in permissions %} - {% if perm.user_id != g.user.id %} {# if user is logged in, only display his form below, not a row #} - {{ permissions_table_row_other_user(perm, permissions_applier_form, permissions_form) }} - {% endif %} - {% endfor %} - {{ permissions_table_row_current_user(current_user_permissions, permissions_applier_form, permissions_form) }} -
UsernameIs BuilderIs Admin
- {% if g.user and (permissions_applier_form or permissions_form) %} {# TODO: when to display? #} - -
- {% endif %} - {% else %} - No permissions for other users for this Copr. - {% endif %} -{% endmacro %} - -{% macro permissions_table_row_other_user(perm, permissions_applier_form, permissions_form) %} - - {{ perm.user.name }} - - {% if permissions_form %} - {% if perm.copr_builder != 0 %} - {{ permissions_form['copr_builder_{0}'.format(perm.user.id)] }} - {% endif %} - {% else %} - {{ perm.copr_builder|perm_type_from_num }} - {% endif %} - - - {% if permissions_form %} - {% if perm.copr_admin != 0 %} - {{ permissions_form['copr_admin_{0}'.format(perm.user.id)] }} - {% endif %} - {% else %} - {{ perm.copr_admin|perm_type_from_num }} - {% endif %} - - -{% endmacro %} - -{% macro permissions_table_row_current_user(current_user_permissions, permissions_applier_form, permissions_form) %} - {# if user is logged in and permissions_applier_form is defined, display it #} - {% if g.user and permissions_applier_form %} - - {{ g.user.name }} - - {% if current_user_permissions %} - {{ current_user_permissions.copr_builder|perm_type_from_num }} - {% else %} - Not requested - {% endif %} -
- {{ permissions_applier_form.copr_builder|safe }} - - - {% if current_user_permissions %} - {{ current_user_permissions.copr_admin|perm_type_from_num }} - {% else %} - Not requested - {% endif %} -
- {{ permissions_applier_form.copr_admin|safe }} - - {% endif %} - - {# if user is admin (means current_user_permissions is set), display his own permissions for changing #} - {% if g.user and permissions_form and current_user_permissions %} - - {{ g.user.name }} - - {{ permissions_form['copr_builder_{0}'.format(g.user.id)] }} - - - {{ permissions_form['copr_admin_{0}'.format(g.user.id)] }} - - - {% endif %} -{% endmacro %} diff --git a/coprs_frontend/coprs/templates/coprs/detail/add_build.html b/coprs_frontend/coprs/templates/coprs/detail/add_build.html deleted file mode 100644 index c0b1b61..0000000 --- a/coprs_frontend/coprs/templates/coprs/detail/add_build.html +++ /dev/null @@ -1,10 +0,0 @@ -{% extends "coprs/detail.html" %} -{% block title %}Adding Build for {{ copr.owner.name }}/{{ copr.name }}{% endblock %} -{% block new_build_selected %}selected{% endblock %} -{% from "coprs/detail/_builds_forms.html" import copr_build_form with context %} - -{% block detail_body %} - - {{ copr_build_form(form, 'coprs_ns.copr_new_build', copr) }} - -{% endblock %} diff --git a/coprs_frontend/coprs/templates/coprs/detail/builds.html b/coprs_frontend/coprs/templates/coprs/detail/builds.html deleted file mode 100644 index 3aca8bf..0000000 --- a/coprs_frontend/coprs/templates/coprs/detail/builds.html +++ /dev/null @@ -1,16 +0,0 @@ -{% extends "coprs/detail.html" %} -{% block title %}Builds for {{ copr.owner.name }}/{{ copr.name }}{% endblock %} -{% block builds_selected %}selected{% endblock %} -{% from "_helpers.html" import render_pagination %} -{% from "coprs/detail/_builds_table.html" import builds_table with context %} - -{% block detail_body %} - {% if builds %} - {{ builds_table(builds, paginator.page) }} - - {% else %} -

No builds so far.

- {% endif %} -{% endblock %} diff --git a/coprs_frontend/coprs/templates/coprs/detail/delete.html b/coprs_frontend/coprs/templates/coprs/detail/delete.html deleted file mode 100644 index f0e22b9..0000000 --- a/coprs_frontend/coprs/templates/coprs/detail/delete.html +++ /dev/null @@ -1,14 +0,0 @@ -{% extends "coprs/detail.html" %} -{% block title %}Delete {{ copr.owner.name }}/{{ copr.name }}?{% endblock %} -{% block delete_selected %}selected{% endblock %} -{% from "coprs/_coprs_forms.html" import copr_delete_form %} - -{% block detail_body %} -

If you really want to delete this Project, you'll have to answer this riddle:

-

{{ range(5)|random }}.{{ range(10)|random }} hens lay {{ range(5)|random }}.{{ range(10)|random }} eggs in - {{ range(5)|random }}.{{ range(10)|random }} days. How many eggs do {{ range(5)|random }}.{{ range(10)|random }} - hens lay in {{ range(5)|random }}.{{ range(10)|random }} days?

-

Ok, kidding, just type "yes" into the below box.

- {{ copr_delete_form(form, copr) }} - -{% endblock %} diff --git a/coprs_frontend/coprs/templates/coprs/detail/edit.html b/coprs_frontend/coprs/templates/coprs/detail/edit.html deleted file mode 100644 index 9b82fe6..0000000 --- a/coprs_frontend/coprs/templates/coprs/detail/edit.html +++ /dev/null @@ -1,10 +0,0 @@ -{% extends "coprs/detail.html" %} -{% block title %}Editing {{ copr.owner.name }}/{{ copr.name }}{% endblock %} -{% block edit_selected %}selected{% endblock %} -{% from "coprs/_coprs_forms.html" import copr_form, copr_permissions_form with context %} - -{% block detail_body %} - - {{ copr_form(form, view = 'coprs_ns.copr_update', copr = copr) }} - -{% endblock %} diff --git a/coprs_frontend/coprs/templates/coprs/detail/edit2.html b/coprs_frontend/coprs/templates/coprs/detail/edit2.html deleted file mode 100644 index 3127b20..0000000 --- a/coprs_frontend/coprs/templates/coprs/detail/edit2.html +++ /dev/null @@ -1,13 +0,0 @@ -{% extends "coprs/detail.html" %} - -{% block detail_body %} - -
- -
- {% block detail_body2 %}{% endblock %} -{% endblock %} diff --git a/coprs_frontend/coprs/templates/coprs/detail/edit_chroot.html b/coprs_frontend/coprs/templates/coprs/detail/edit_chroot.html deleted file mode 100644 index fac9a65..0000000 --- a/coprs_frontend/coprs/templates/coprs/detail/edit_chroot.html +++ /dev/null @@ -1,19 +0,0 @@ -{% extends "coprs/detail/edit2.html" %} -{% from "_helpers.html" import render_field %} -{% block title %}Editing {{ copr.owner.name }}/{{ copr.name }}/{{ chroot.name }}{% endblock %} -{% block edit_selected %}selected{% endblock %} -{% block edit_chroot_selected %}selected{% endblock %} - -{% block detail_body2 %} - -

Edit chroot '{{ chroot.name }}'

-
-
- {{ form.csrf_token }} - {{ render_field(form.buildroot_pkgs, size=80, placeholder='Space separated list of packages. E.g.: scl-utils-build ruby193-build') }} -
-
-
- - -{% endblock %} diff --git a/coprs_frontend/coprs/templates/coprs/detail/monitor.html b/coprs_frontend/coprs/templates/coprs/detail/monitor.html deleted file mode 100644 index a2d9ab6..0000000 --- a/coprs_frontend/coprs/templates/coprs/detail/monitor.html +++ /dev/null @@ -1,41 +0,0 @@ -{% extends "coprs/detail.html" %} -{% block title %}Monitor {{ copr.owner.name }}/{{ copr.name }}{% endblock %} -{% block monitor_selected %}selected{% endblock %} - -{% block detail_body %} - {% if build %} -

- Latest build status: - {{ build.state }} -

- - - - {% for chroot in chroots %} - - {% endfor %} - - {% for package, states in packages %} - - - {% for build_id, state in states %} - - {% endfor %} - - {% endfor %} -
Package - {{ chroot }} -
{{ package }} - {% if state %} - {{ state }} - {% else %} - resubmit - {% endif %} -
- {% else %} -

No builds so far.

- {% endif %} -{% endblock %} diff --git a/coprs_frontend/coprs/templates/coprs/detail/overview.html b/coprs_frontend/coprs/templates/coprs/detail/overview.html deleted file mode 100644 index d88cb12..0000000 --- a/coprs_frontend/coprs/templates/coprs/detail/overview.html +++ /dev/null @@ -1,76 +0,0 @@ -{% extends "coprs/detail.html" %} - -{% from "coprs/_coprs_forms.html" import copr_legal_flag_form with context %} - -{% block overview_selected %}selected{% endblock %} - -{% block detail_body %} -

Description

-
{{ copr.description|markdown|default('Description not filled in by author. Very likely personal repository for testing purpose, which you should not use.', true) }}
-

Installation Instructions

-
{{ copr.instructions|markdown|default('Instructions not filled in by author. Author knows what to do. Everybody else should avoid this repo.', true) }}
-

Active Releases

-
-

- The following unofficial repositories are provided as-is by owner of this project. - Contact the owner directly for bugs or issues (IE: not bugzilla). -

-
- - - - - - - {% for mock_chroot in copr.active_chroots %} - {% if loop.index < copr.active_chroots|length %} - {% if mock_chroot.os_release != copr.active_chroots[loop.index].os_release or - mock_chroot.os_version != copr.active_chroots[loop.index].os_version %} - {# next release is different => release-end #} - - {% else %} - - {% endif %} - {% else %}{# last line => release-end for sure #} - - {% endif %} - {% if mock_chroot.os_release != copr.active_chroots[loop.index0 - 1].os_release or - mock_chroot.os_version != copr.active_chroots[loop.index0 - 1].os_version or - loop.index0 == 0 %} - {# previous os_release-os_version were different or this is the first one #} - - {% else %} - - {% endif %} - - {% if mock_chroot.os_release != copr.active_chroots[loop.index0 - 1].os_release or - mock_chroot.os_version != copr.active_chroots[loop.index0 - 1].os_version or - loop.index0 == 0 %} - {# previous os_release-os_version were different or this is the first one #} - - {% else %} - - {% endif %} - - - {% else %} - - {% endfor %} -
ReleaseArchitectureYum Repo
{{ mock_chroot.os_release|capitalize }} {{ mock_chroot.os_version }}{{ mock_chroot.arch }} - {{ copr.owner.name }}-{{ copr.name }}.repo
No active releases
- {% if copr.repos_list %} -

Repository List

- - {% endif %} - -
- {{ copr_legal_flag_form(form, copr) }} -{% endblock %} diff --git a/coprs_frontend/coprs/templates/coprs/detail/permissions.html b/coprs_frontend/coprs/templates/coprs/detail/permissions.html deleted file mode 100644 index 3d9cb75..0000000 --- a/coprs_frontend/coprs/templates/coprs/detail/permissions.html +++ /dev/null @@ -1,13 +0,0 @@ -{% extends "coprs/detail.html" %} -{% block title %}Permissions for {{ copr.owner.name }}/{{ copr.name }}{% endblock %} -{% block permissions_selected %}selected{% endblock %} -{% from "coprs/detail/_permissions_table.html" import permissions_table with context%} - -{% block detail_body %} - {% if (g.user and g.user != copr.owner) or permissions %} - {# the table is displayed only if there are some permissions or a non-owner is viewing the page (then display at least his applier form #} - {{ permissions_table(permissions, current_user_permissions, copr, permissions_applier_form, permissions_form) }} - {% else %} -

No permissions yet

- {% endif %} -{% endblock %} diff --git a/coprs_frontend/coprs/templates/coprs/show.html b/coprs_frontend/coprs/templates/coprs/show.html deleted file mode 100644 index a3d041e..0000000 --- a/coprs_frontend/coprs/templates/coprs/show.html +++ /dev/null @@ -1,61 +0,0 @@ -{% extends "layout.html" %} -{% block title %}Project List{% endblock %} -{% block header %}Project List{% endblock %} -{% from "_helpers.html" import render_pagination %} -{% block body %} - {% if g.user %} -
- - User Image - -

{{ g.user.name }}

-

- {{ g.user.coprs_count }} -

-

projects

-
- {% endif %} - {% if not g.user and not fulltext%} -
-

Copr is an easy-to-use automatic build system providing a package repository as its output.

-

Start with making your own repository in these three steps:

-
    -
  1. choose an architecture and system you want to build for
  2. -
  3. provide Copr with src.rpm packages available online
  4. -
  5. let Coper do all the work and wait for your new repo
  6. -
-

For more information please visit Copr wiki

-
- {% endif %} - -
- {% if g.user %} - - {% endif %} - {% if fulltext %} -
Displaying results for search "{{ fulltext }}"
- {% endif %} - {% for copr in coprs %} -
- {{ copr.owner.name }}/{{ copr.name }} -

{{ copr.description|markdown|default('Description not filled in by author. Very likely personal repository for testing purpose, which you should not use.', true) }}

-

- {% for mock_chroot in copr.active_chroots %} - {{ mock_chroot.os_release|os_name_short(mock_chroot.os_version) }}.{{ mock_chroot.arch }}{% if not loop.last %}, {% endif %} - {% endfor %} -

-
- {% else %} -

No projects...

- {% endfor %} - -
-{% endblock %} diff --git a/coprs_frontend/coprs/templates/layout.html b/coprs_frontend/coprs/templates/layout.html deleted file mode 100644 index 5a607bc..0000000 --- a/coprs_frontend/coprs/templates/layout.html +++ /dev/null @@ -1,45 +0,0 @@ - - - - {% block title %}Coprs Build System{% endblock %} - - - - - - - - -
- {% for message in get_flashed_messages() %} -
{{ message }}
- {% endfor %} -
- {% block body %}{% endblock %} -
-
- - - diff --git a/coprs_frontend/coprs/templates/login.html b/coprs_frontend/coprs/templates/login.html deleted file mode 100644 index 7cad08a..0000000 --- a/coprs_frontend/coprs/templates/login.html +++ /dev/null @@ -1,16 +0,0 @@ -{% extends "layout.html" %} -{% block title %}Sign in Coprs{% endblock %} -{% block header %}Sign in Coprs Build System{% endblock %} -{% block body %} - {% if error %}

Error: {{ error }}

{% endif %} - -
-

Fedora Accounts System login

-
- Username: - - - -
-
-{% endblock %} diff --git a/coprs_frontend/coprs/views/__init__.py b/coprs_frontend/coprs/views/__init__.py deleted file mode 100644 index e69de29..0000000 --- a/coprs_frontend/coprs/views/__init__.py +++ /dev/null diff --git a/coprs_frontend/coprs/views/admin_ns/__init__.py b/coprs_frontend/coprs/views/admin_ns/__init__.py deleted file mode 100644 index 8820bd1..0000000 --- a/coprs_frontend/coprs/views/admin_ns/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -import flask - -admin_ns = flask.Blueprint("admin_ns", __name__, url_prefix="/admin") diff --git a/coprs_frontend/coprs/views/admin_ns/admin_general.py b/coprs_frontend/coprs/views/admin_ns/admin_general.py deleted file mode 100644 index 1f91370..0000000 --- a/coprs_frontend/coprs/views/admin_ns/admin_general.py +++ /dev/null @@ -1,44 +0,0 @@ -import time - -import flask - -from coprs import db -from coprs import helpers -from coprs import models - -from coprs.views.admin_ns import admin_ns -from coprs.views.misc import login_required - - -@admin_ns.route("/") -@login_required(role=helpers.RoleEnum("admin")) -def admin_index(): - return flask.render_template("admin/index.html") - - -@admin_ns.route("/legal-flag/") -@login_required(role=helpers.RoleEnum("admin")) -def legal_flag(): - legal_flags = (models.LegalFlag.query - .outerjoin(models.LegalFlag.copr) - .options(db.contains_eager(models.LegalFlag.copr)) - .filter(models.LegalFlag.resolved_on == None) - .order_by(models.LegalFlag.raised_on.desc()) - .all()) - - return flask.render_template("admin/legal-flag.html", - legal_flags=legal_flags) - - -@admin_ns.route("/legal-flag//resolve/", methods=["POST"]) -@login_required(role=helpers.RoleEnum("admin")) -def legal_flag_resolve(flag_id): - - (models.LegalFlag.query - .filter(models.LegalFlag.id == flag_id) - .update({"resolved_on": int(time.time()), - "resolver_id": flask.g.user.id})) - - db.session.commit() - flask.flash("Legal flag resolved") - return flask.redirect(flask.url_for("admin_ns.legal_flag")) diff --git a/coprs_frontend/coprs/views/api_ns/__init__.py b/coprs_frontend/coprs/views/api_ns/__init__.py deleted file mode 100644 index 11e81ae..0000000 --- a/coprs_frontend/coprs/views/api_ns/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -import flask - -api_ns = flask.Blueprint("api_ns", __name__, url_prefix="/api") diff --git a/coprs_frontend/coprs/views/api_ns/api_general.py b/coprs_frontend/coprs/views/api_ns/api_general.py deleted file mode 100644 index f792977..0000000 --- a/coprs_frontend/coprs/views/api_ns/api_general.py +++ /dev/null @@ -1,418 +0,0 @@ -import base64 -import datetime -import urlparse - -import flask - -from coprs import db -from coprs import exceptions -from coprs import forms -from coprs import helpers - -from coprs.views.misc import login_required, api_login_required - -from coprs.views.api_ns import api_ns - -from coprs.logic import builds_logic -from coprs.logic import coprs_logic - - -@api_ns.route("/") -def api_home(): - """ - Render the home page of the api. - This page provides information on how to call/use the API. - """ - - return flask.render_template("api.html") - - -@api_ns.route("/new/", methods=["GET", "POST"]) -@login_required -def api_new_token(): - """ - Generate a new API token for the current user. - """ - - user = flask.g.user - copr64 = base64.b64encode("copr") + "##" - api_login = helpers.generate_api_token( - flask.current_app.config["API_TOKEN_LENGTH"] - len(copr64)) - user.api_login = api_login - user.api_token = helpers.generate_api_token( - flask.current_app.config["API_TOKEN_LENGTH"]) - user.api_token_expiration = datetime.date.today() + \ - datetime.timedelta( - days=flask.current_app.config["API_TOKEN_EXPIRATION"]) - - db.session.add(user) - db.session.commit() - return flask.redirect(flask.url_for("api_ns.api_home")) - - -@api_ns.route("/coprs//new/", methods=["POST"]) -@api_login_required -def api_new_copr(username): - """ - Receive information from the user on how to create its new copr, - check their validity and create the corresponding copr. - - :arg name: the name of the copr to add - :arg chroots: a comma separated list of chroots to use - :kwarg repos: a comma separated list of repository that this copr - can use. - :kwarg initial_pkgs: a comma separated list of initial packages to - build in this new copr - - """ - - form = forms.CoprFormFactory.create_form_cls()(csrf_enabled=False) - httpcode = 200 - if form.validate_on_submit(): - infos = [] - try: - copr = coprs_logic.CoprsLogic.add( - name=form.name.data.strip(), - repos=" ".join(form.repos.data.split()), - user=flask.g.user, - selected_chroots=form.selected_chroots, - description=form.description.data, - instructions=form.instructions.data, - check_for_duplicates=True) - infos.append("New project was successfully created.") - - if form.initial_pkgs.data: - builds_logic.BuildsLogic.add( - user=flask.g.user, - pkgs=" ".join(form.initial_pkgs.data.split()), - copr=copr) - - infos.append("Initial packages were successfully " - "submitted for building.") - - output = {"output": "ok", "message": "\n".join(infos)} - db.session.commit() - except exceptions.DuplicateException as err: - output = {"output": "notok", "error": err} - httpcode = 500 - db.session.rollback() - - else: - errormsg = "Validation error\n" - if form.errors: - for field, emsgs in form.errors.items(): - errormsg += "- {0}: {1}\n".format(field, "\n".join(emsgs)) - - errormsg = errormsg.replace('"', "'") - output = {"output": "notok", "error": errormsg} - httpcode = 500 - - jsonout = flask.jsonify(output) - jsonout.status_code = httpcode - return jsonout - - -@api_ns.route("/coprs/") -@api_ns.route("/coprs//") -def api_coprs_by_owner(username=None): - """ Return the list of coprs owned by the given user. - username is taken either from GET params or from the URL itself - (in this order). - - :arg username: the username of the person one would like to the - coprs of. - - """ - username = flask.request.args.get("username", None) or username - release_tmpl = "{chroot.os_release}-{chroot.os_version}-{chroot.arch}" - httpcode = 200 - if username: - query = coprs_logic.CoprsLogic.get_multiple( - flask.g.user, user_relation="owned", - username=username, with_builds=True) - - repos = query.all() - output = {"output": "ok", "repos": []} - for repo in repos: - yum_repos = {} - for build in repo.builds: - if build.results: - for chroot in repo.active_chroots: - release = release_tmpl.format(chroot=chroot) - yum_repos[release] = urlparse.urljoin( - build.results, release + '/') - break - - output["repos"].append({"name": repo.name, - "additional_repos": repo.repos, - "yum_repos": yum_repos, - "description": repo.description, - "instructions": repo.instructions}) - else: - output = {"output": "notok", "error": "Invalid request"} - httpcode = 500 - - jsonout = flask.jsonify(output) - jsonout.status_code = httpcode - return jsonout - -@api_ns.route("/coprs///detail/") -def api_coprs_by_owner_detail(username, coprname): - """ Return detail of one project. - - :arg username: the username of the person one would like to the - coprs of. - :arg coprname: the name of project. - - """ - copr = coprs_logic.CoprsLogic.get(flask.g.user, username, - coprname).first() - release_tmpl = "{chroot.os_release}-{chroot.os_version}-{chroot.arch}" - httpcode = 200 - if username and copr: - output = {"output": "ok", "detail": {}} - yum_repos = {} - for build in copr.builds: - if build.results: - for chroot in copr.active_chroots: - release = release_tmpl.format(chroot=chroot) - yum_repos[release] = urlparse.urljoin( - build.results, release + '/') - break - output["detail"] = {"name": copr.name, - "additional_repos": copr.repos, - "yum_repos": yum_repos, - "description": copr.description, - "instructions": copr.instructions, - "last_modified": builds_logic.BuildsLogic.last_modified(copr)} - else: - output = {"output": "notok", "error": "Copr with name {0} does not exist.".format(coprname)} - httpcode = 500 - - jsonout = flask.jsonify(output) - jsonout.status_code = httpcode - return jsonout - -@api_ns.route("/coprs///new_build/", methods=["POST"]) -@api_login_required -def copr_new_build(username, coprname): - form = forms.BuildForm(csrf_enabled=False) - copr = coprs_logic.CoprsLogic.get(flask.g.user, username, - coprname).first() - httpcode = 200 - if not copr: - output = {"output": "notok", "error": - "Copr with name {0} does not exist.".format(coprname)} - httpcode = 500 - - else: - if form.validate_on_submit() and flask.g.user.can_build_in(copr): - # we're checking authorization above for now - build = builds_logic.BuildsLogic.add( - user=flask.g.user, - pkgs=form.pkgs.data.replace('\n', ' '), - copr=copr) - - if flask.g.user.proven: - build.memory_reqs = form.memory_reqs.data - build.timeout = form.timeout.data - - db.session.commit() - - output = {"output": "ok", - "id": build.id, - "message": "Build was added to {0}.".format(coprname)} - else: - output = {"output": "notok", "error": "Invalid request"} - httpcode = 500 - - jsonout = flask.jsonify(output) - jsonout.status_code = httpcode - return jsonout - - -@api_ns.route("/coprs/build_status//", methods=["GET"]) -@api_login_required -def build_status(build_id): - if build_id.isdigit(): - build = builds_logic.BuildsLogic.get(build_id).first() - else: - build = None - - if build: - httpcode = 200 - output = {"output": "ok", - "status": build.state} - else: - output = {"output": "notok", "error": "Invalid build"} - httpcode = 404 - - jsonout = flask.jsonify(output) - jsonout.status_code = httpcode - return jsonout - -@api_ns.route("/coprs/build_detail//", methods=["GET"]) -def build_detail(build_id): - if build_id.isdigit(): - build = builds_logic.BuildsLogic.get(build_id).first() - else: - build = None - - if build: - httpcode = 200 - output = {"output": "ok", - "owner": build.copr.owner.name, - "project": build.copr.name, - "status": build.state} - else: - output = {"output": "notok", "error": "Invalid build"} - httpcode = 404 - - jsonout = flask.jsonify(output) - jsonout.status_code = httpcode - return jsonout - -@api_ns.route("/coprs/cancel_build//", methods=["POST"]) -@api_login_required -def cancel_build(build_id): - if build_id.isdigit(): - build = builds_logic.BuildsLogic.get(build_id).first() - else: - build = None - - if build: - try: - builds_logic.BuildsLogic.cancel_build(flask.g.user, build) - except InsufficientRightsException as e: - output = {'output': 'notok', 'error': str(e)} - httpcode = 500 - else: - db.session.commit() - httpcode = 200 - output = {'output': 'ok', status: "Build canceled"} - else: - output = {"output": "notok", "error": "Invalid build"} - httpcode = 404 - jsonout = flask.jsonify(output) - jsonout.status_code = httpcode - return jsonout - -@api_ns.route('/coprs///modify/', methods=["POST"]) -@api_login_required -def copr_modify(username, coprname): - form = forms.CoprModifyForm(csrf_enabled=False) - copr = coprs_logic.CoprsLogic.get(flask.g.user, username, coprname).first() - - if copr is None: - output = {'output': 'notok', 'error': 'Invalid copr name or username'} - httpcode = 500 - elif not form.validate_on_submit(): - output = {'output': 'notok', 'error': 'Invalid request'} - httpcode = 500 - else: - # .raw_data needs to be inspected to figure out whether the field - # was not sent or was sent empty - if form.description.raw_data and len(form.description.raw_data): - copr.description = form.description.data - if form.instructions.raw_data and len(form.instructions.raw_data): - copr.instructions = form.instructions.data - if form.repos.raw_data and len(form.repos.raw_data): - copr.repos = form.repos.data - - try: - coprs_logic.CoprsLogic.update(flask.g.user, copr) - except (exceptions.ActionInProgressException, exceptions.InsufficientRightsException) as e: - db.session.rollback() - - output = {'output': 'notok', 'error': str(e)} - httpcode = 500 - else: - db.session.commit() - - output = {'output': 'ok', - 'description': copr.description, - 'instructions': copr.instructions, - 'repos': copr.repos} - httpcode = 200 - - jsonout = flask.jsonify(output) - jsonout.status_code = httpcode - return jsonout - -@api_ns.route('/coprs///modify//', methods=["POST"]) -@api_login_required -def copr_modify_chroot(username, coprname, chrootname): - form = forms.ModifyChrootForm(csrf_enabled=False) - copr = coprs_logic.CoprsLogic.get(flask.g.user, username, coprname).first() - chroot = coprs_logic.MockChrootsLogic.get_from_name(chrootname, active_only=True).first() - - if copr is None: - output = {'output': 'notok', 'error': 'Invalid copr name or username'} - httpcode = 500 - elif chroot is None: - output = {'output': 'notok', 'error': 'Invalid chroot name'} - httpcode = 500 - elif not form.validate_on_submit(): - output = {'output': 'notok', 'error': 'Invalid request'} - httpcode = 500 - else: - coprs_logic.CoprChrootsLogic.update_buildroot_pkgs(copr, chroot, form.buildroot_pkgs.data) - db.session.commit() - - ch = copr.check_copr_chroot(chroot) - output = {'output': 'ok', 'buildroot_pkgs': ch.buildroot_pkgs} - httpcode = 200 - - jsonout = flask.jsonify(output) - jsonout.status_code = httpcode - return jsonout - -@api_ns.route('/coprs///detail//', methods=["GET"]) -def copr_chroot_details(username, coprname, chrootname): - copr = coprs_logic.CoprsLogic.get(flask.g.user, username, coprname).first() - chroot = coprs_logic.MockChrootsLogic.get_from_name(chrootname, active_only=True).first() - - if copr is None: - output = {'output': 'notok', 'error': 'Invalid copr name or username'} - httpcode = 500 - elif chroot is None: - output = {'output': 'notok', 'error': 'Invalid chroot name'} - httpcode = 500 - else: - ch = copr.check_copr_chroot(chroot) - output = {'output': 'ok', 'buildroot_pkgs': ch.buildroot_pkgs} - httpcode = 200 - - jsonout = flask.jsonify(output) - jsonout.status_code = httpcode - return jsonout - -@api_ns.route("/coprs/search/") -@api_ns.route("/coprs/search//") -def api_coprs_search_by_project(project=None): - """ Return the list of coprs found in search by the given text. - project is taken either from GET params or from the URL itself - (in this order). - - :arg project: the text one would like find for coprs. - - """ - project = flask.request.args.get("project", None) or project - httpcode = 200 - if project: - query = coprs_logic.CoprsLogic.get_multiple_fulltext( - flask.g.user, project) - - repos = query.all() - output = {"output": "ok", "users": []} - for repo in repos: - output["repos"].append({"username": repo.owner, - "coprname": repo.name, - "description": repo.description}) - else: - output = {"output": "notok", "error": "Invalid request"} - httpcode = 500 - - jsonout = flask.jsonify(output) - jsonout.status_code = httpcode - return jsonout diff --git a/coprs_frontend/coprs/views/backend_ns/__init__.py b/coprs_frontend/coprs/views/backend_ns/__init__.py deleted file mode 100644 index f527c95..0000000 --- a/coprs_frontend/coprs/views/backend_ns/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -import flask - -backend_ns = flask.Blueprint("backend_ns", __name__, url_prefix="/backend") diff --git a/coprs_frontend/coprs/views/backend_ns/backend_general.py b/coprs_frontend/coprs/views/backend_ns/backend_general.py deleted file mode 100644 index 1a9f473..0000000 --- a/coprs_frontend/coprs/views/backend_ns/backend_general.py +++ /dev/null @@ -1,90 +0,0 @@ -import flask -import sys -import time - -from coprs import db -from coprs.logic import actions_logic -from coprs.logic import builds_logic - -from coprs.views import misc -from coprs.views.backend_ns import backend_ns -from whoosh.index import LockError - - -@backend_ns.route("/waiting/") -@misc.backend_authenticated -def waiting(): - """ - Return list of waiting actions and builds. - """ - - # models.Actions - actions_list = [action.to_dict( - options={"__columns_except__": ["result", "message", "ended_on"]}) - for action in actions_logic.ActionsLogic.get_waiting() - ] - - # models.Builds - builds_list = [] - - for build in builds_logic.BuildsLogic.get_waiting(): - build_dict = build.to_dict( - options={"copr": {"owner": {}, - "__columns_only__": ["id", "name"], - "__included_ids__": False - }, - "__included_ids__": False}) - - # return separate build for each chroot this build - # is assigned with - for chroot in build.chroots: - build_dict_copy = build_dict.copy() - build_dict_copy["chroot"] = chroot.name - build_dict_copy[ - "buildroot_pkgs"] = build.copr.buildroot_pkgs(chroot) - builds_list.append(build_dict_copy) - - return flask.jsonify({"actions": actions_list, "builds": builds_list}) - - -@backend_ns.route("/update/", methods=["POST", "PUT"]) -@misc.backend_authenticated -def update(): - result = {} - - for typ, logic_cls in [("actions", actions_logic.ActionsLogic), - ("builds", builds_logic.BuildsLogic)]: - - if typ not in flask.request.json: - continue - - to_update = {} - for obj in flask.request.json[typ]: - to_update[obj["id"]] = obj - - existing = {} - for obj in logic_cls.get_by_ids(to_update.keys()).all(): - existing[obj.id] = obj - - non_existing_ids = list(set(to_update.keys()) - set(existing.keys())) - - for i, obj in existing.items(): - logic_cls.update_state_from_dict(obj, to_update[i]) - - i = 5 - exc_info = None - while i > 0: - try: - db.session.commit() - i = -100 - except LockError: - i -= 1 - exc_info = sys.exc_info()[2] - time.sleep(5) - if i != -100: - raise LockError, None, exc_info - - result.update({"updated_{0}_ids".format(typ): list(existing.keys()), - "non_existing_{0}_ids".format(typ): non_existing_ids}) - - return flask.jsonify(result) diff --git a/coprs_frontend/coprs/views/coprs_ns/__init__.py b/coprs_frontend/coprs/views/coprs_ns/__init__.py deleted file mode 100644 index 94372a2..0000000 --- a/coprs_frontend/coprs/views/coprs_ns/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -import flask - -coprs_ns = flask.Blueprint("coprs_ns", __name__, url_prefix="/coprs") diff --git a/coprs_frontend/coprs/views/coprs_ns/coprs_builds.py b/coprs_frontend/coprs/views/coprs_ns/coprs_builds.py deleted file mode 100644 index dbcc3dd..0000000 --- a/coprs_frontend/coprs/views/coprs_ns/coprs_builds.py +++ /dev/null @@ -1,176 +0,0 @@ -import flask - -from coprs import db -from coprs import forms -from coprs import helpers - -from coprs.logic import builds_logic -from coprs.logic import coprs_logic - -from coprs.views.misc import login_required, page_not_found -from coprs.views.coprs_ns import coprs_ns - -from coprs.exceptions import (ActionInProgressException, - InsufficientRightsException) - - -@coprs_ns.route("///builds/", defaults={"page": 1}) -@coprs_ns.route("///builds//") -def copr_builds(username, coprname, page=1): - copr = coprs_logic.CoprsLogic.get(flask.g.user, username, coprname).first() - - if not copr: - return page_not_found( - "Copr with name {0} does not exist.".format(coprname)) - - builds_query = builds_logic.BuildsLogic.get_multiple( - flask.g.user, copr=copr) - - paginator = helpers.Paginator( - builds_query, copr.build_count, page, per_page_override=10) - - return flask.render_template("coprs/detail/builds.html", - copr=copr, - builds=paginator.sliced_query, - paginator=paginator) - - -@coprs_ns.route("///add_build/") -@login_required -def copr_add_build(username, coprname, form=None): - copr = coprs_logic.CoprsLogic.get(flask.g.user, username, coprname).first() - - if not copr: - return page_not_found( - "Copr with name {0} does not exist.".format(coprname)) - - if not form: - form = forms.BuildForm() - - return flask.render_template("coprs/detail/add_build.html", - copr=copr, - form=form) - - -@coprs_ns.route("///new_build/", methods=["POST"]) -@login_required -def copr_new_build(username, coprname): - form = forms.BuildForm() - copr = coprs_logic.CoprsLogic.get(flask.g.user, username, coprname).first() - if not copr: - return page_not_found( - "Copr with name {0} does not exist.".format(coprname)) - - if form.validate_on_submit(): - try: - build = builds_logic.BuildsLogic.add(user=flask.g.user, - pkgs=form.pkgs.data.replace( - "\n", " "), - copr=copr) - if flask.g.user.proven: - build.memory_reqs = form.memory_reqs.data - build.timeout = form.timeout.data - - except (ActionInProgressException, InsufficientRightsException) as e: - flask.flash(str(e)) - db.session.rollback() - else: - flask.flash("Build was added") - db.session.commit() - - return flask.redirect(flask.url_for("coprs_ns.copr_builds", - username=username, - coprname=copr.name)) - else: - return copr_add_build(username=username, coprname=coprname, form=form) - - -@coprs_ns.route("///cancel_build//", - defaults={"page": 1}, - methods=["POST"]) -@coprs_ns.route("///cancel_build///", - methods=["POST"]) -@login_required -def copr_cancel_build(username, coprname, build_id, page=1): - # only the user who ran the build can cancel it - build = builds_logic.BuildsLogic.get(build_id).first() - if not build: - return page_not_found( - "Build with id {0} does not exist.".format(build_id)) - try: - builds_logic.BuildsLogic.cancel_build(flask.g.user, build) - except InsufficientRightsException as e: - flask.flash(str(e)) - else: - db.session.commit() - flask.flash("Build was canceled") - - return flask.redirect(flask.url_for("coprs_ns.copr_builds", - username=username, - coprname=coprname, - page=page)) - - -@coprs_ns.route("///repeat_build//", - defaults={"page": 1}, - methods=["GET", "POST"]) -@coprs_ns.route("///repeat_build///", - methods=["GET", "POST"]) -@login_required -def copr_repeat_build(username, coprname, build_id, page=1): - build = builds_logic.BuildsLogic.get(build_id).first() - copr = coprs_logic.CoprsLogic.get( - flask.g.user, username=username, coprname=coprname).first() - - if not build: - return page_not_found( - "Build with id {0} does not exist.".format(build_id)) - - if not copr: - return page_not_found( - "Copr {0}/{1} does not exist.".format(username, coprname)) - - try: - builds_logic.BuildsLogic.add( - user=flask.g.user, - pkgs=build.pkgs, - copr=copr, - repos=build.repos, - memory_reqs=build.memory_reqs, - timeout=build.timeout) - - except (ActionInProgressException, InsufficientRightsException) as e: - db.session.rollback() - flask.flash(str(e)) - else: - db.session.commit() - flask.flash("Build was resubmitted") - - return flask.redirect(flask.url_for("coprs_ns.copr_builds", - username=username, - coprname=coprname, - page=page)) - - -@coprs_ns.route("///delete_build//", - defaults={"page": 1}, - methods=["POST"]) -@coprs_ns.route("///delete_build///", - methods=["POST"]) -@login_required -def copr_delete_build(username, coprname, build_id, page=1): - build = builds_logic.BuildsLogic.get(build_id).first() - if not build: - return page_not_found( - "Build with id {0} does not exist.".format(build_id)) - try: - builds_logic.BuildsLogic.delete_build(flask.g.user, build) - except InsufficientRightsException as e: - flask.flash(str(e)) - else: - db.session.commit() - flask.flash("Build was deleted") - - return flask.redirect(flask.url_for("coprs_ns.copr_builds", - username=username, coprname=coprname, - page=page)) diff --git a/coprs_frontend/coprs/views/coprs_ns/coprs_chroots.py b/coprs_frontend/coprs/views/coprs_ns/coprs_chroots.py deleted file mode 100644 index 21c5267..0000000 --- a/coprs_frontend/coprs/views/coprs_ns/coprs_chroots.py +++ /dev/null @@ -1,75 +0,0 @@ -import flask - -from coprs import db -from coprs import forms - -from coprs.logic import coprs_logic - -from coprs.views.misc import login_required, page_not_found -from coprs.views.coprs_ns import coprs_ns - - -@coprs_ns.route("///edit_chroot//") -@login_required -def chroot_edit(username, coprname, chrootname): - copr = coprs_logic.CoprsLogic.get(flask.g.user, username, coprname).first() - if not copr: - return page_not_found( - "Project with name {0} does not exist.".format(coprname)) - - try: - chroot = coprs_logic.MockChrootsLogic.get_from_name( - chrootname, active_only=True).first() - except ValueError as e: - return page_not_found(str(e)) - - if not chroot: - return page_not_found( - "Chroot name {0} does not exist.".format(chrootname)) - - form = forms.ChrootForm(buildroot_pkgs=copr.buildroot_pkgs(chroot)) - # FIXME - test if chroot belongs to copr - if flask.g.user.can_build_in(copr): - return flask.render_template("coprs/detail/edit_chroot.html", - form=form, copr=copr, chroot=chroot) - else: - return page_not_found( - "You are not allowed to modify chroots in project {0}." - .format(coprname)) - - -@coprs_ns.route("///update_chroot//", - methods=["POST"]) -@login_required -def chroot_update(username, coprname, chrootname): - form = forms.ChrootForm() - copr = coprs_logic.CoprsLogic.get(flask.g.user, username, coprname).first() - if not copr: - return page_not_found( - "Projec with name {0} does not exist.".format(coprname)) - - try: - chroot = coprs_logic.MockChrootsLogic.get_from_name( - chrootname, active_only=True).first() - except ValueError as e: - return page_not_found(str(e)) - - if form.validate_on_submit() and flask.g.user.can_build_in(copr): - coprs_logic.CoprChrootsLogic.update_buildroot_pkgs( - copr, chroot, form.buildroot_pkgs.data) - - flask.flash( - "Buildroot {0} for project {1} was updated".format( - chrootname, coprname)) - - db.session.commit() - - return flask.redirect(flask.url_for("coprs_ns.copr_edit", - username=username, - coprname=copr.name)) - - else: - if form.validate_on_submit(): - flask.flash("You are not allowed to modify chroots.") - else: - return chroot_edit(username, coprname, chrootname) diff --git a/coprs_frontend/coprs/views/coprs_ns/coprs_general.py b/coprs_frontend/coprs/views/coprs_ns/coprs_general.py deleted file mode 100644 index cf56e12..0000000 --- a/coprs_frontend/coprs/views/coprs_ns/coprs_general.py +++ /dev/null @@ -1,502 +0,0 @@ -import os -import time - -import flask -import platform -import smtplib -import sqlalchemy -from email.mime.text import MIMEText - -from coprs import app -from coprs import db -from coprs import exceptions -from coprs import forms -from coprs import helpers -from coprs import models - -from coprs.views.misc import login_required, page_not_found - -from coprs.views.coprs_ns import coprs_ns - -from coprs.logic import builds_logic -from coprs.logic import coprs_logic -from coprs.helpers import parse_package_name, render_repo - - -@coprs_ns.route("/", defaults={"page": 1}) -@coprs_ns.route("//") -def coprs_show(page=1): - query = coprs_logic.CoprsLogic.get_multiple( - flask.g.user, with_mock_chroots=False) - paginator = helpers.Paginator(query, query.count(), page) - - coprs = paginator.sliced_query - return flask.render_template("coprs/show.html", - coprs=coprs, - paginator=paginator) - - -@coprs_ns.route("//", defaults={"page": 1}) -@coprs_ns.route("///") -def coprs_by_owner(username=None, page=1): - query = coprs_logic.CoprsLogic.get_multiple(flask.g.user, - user_relation="owned", - username=username, - with_mock_chroots=False) - - paginator = helpers.Paginator(query, query.count(), page) - - coprs = paginator.sliced_query - return flask.render_template("coprs/show.html", - coprs=coprs, - paginator=paginator) - - -@coprs_ns.route("//allowed/", defaults={"page": 1}) -@coprs_ns.route("//allowed//") -def coprs_by_allowed(username=None, page=1): - query = coprs_logic.CoprsLogic.get_multiple(flask.g.user, - user_relation="allowed", - username=username, - with_mock_chroots=False) - paginator = helpers.Paginator(query, query.count(), page) - - coprs = paginator.sliced_query - return flask.render_template("coprs/show.html", - coprs=coprs, - paginator=paginator) - - -@coprs_ns.route("/fulltext/", defaults={"page": 1}) -@coprs_ns.route("/fulltext//") -def coprs_fulltext_search(page=1): - fulltext = flask.request.args.get("fulltext", "") - try: - query = coprs_logic.CoprsLogic.get_multiple_fulltext( - flask.g.user, fulltext) - except ValueError as e: - flask.flash(str(e)) - return flask.redirect(flask.request.referrer or - flask.url_for("coprs_ns.coprs_show")) - - paginator = helpers.Paginator(query, query.count(), page) - - coprs = paginator.sliced_query - return flask.render_template("coprs/show.html", - coprs=coprs, - paginator=paginator, - fulltext=fulltext) - - -@coprs_ns.route("//add/") -@login_required -def copr_add(username): - form = forms.CoprFormFactory.create_form_cls()() - - return flask.render_template("coprs/add.html", form=form) - - -@coprs_ns.route("//new/", methods=["POST"]) -@login_required -def copr_new(username): - """ - Receive information from the user on how to create its new copr - and create it accordingly. - """ - - form = forms.CoprFormFactory.create_form_cls()() - if form.validate_on_submit(): - copr = coprs_logic.CoprsLogic.add( - flask.g.user, - name=form.name.data, - repos=form.repos.data.replace("\n", " "), - selected_chroots=form.selected_chroots, - description=form.description.data, - instructions=form.instructions.data) - - db.session.commit() - flask.flash("New project was successfully created.") - - if form.initial_pkgs.data: - builds_logic.BuildsLogic.add( - flask.g.user, - pkgs=form.initial_pkgs.data.replace("\n", " "), - copr=copr) - - db.session.commit() - flask.flash("Initial packages were successfully submitted " - "for building.") - - return flask.redirect(flask.url_for("coprs_ns.copr_detail", - username=flask.g.user.name, - coprname=copr.name)) - else: - return flask.render_template("coprs/add.html", form=form) - - -@coprs_ns.route("///") -def copr_detail(username, coprname): - query = coprs_logic.CoprsLogic.get( - flask.g.user, username, coprname, with_mock_chroots=True) - form = forms.CoprLegalFlagForm() - try: - copr = query.one() - except sqlalchemy.orm.exc.NoResultFound: - return page_not_found( - "Copr with name {0} does not exist.".format(coprname)) - - return flask.render_template("coprs/detail/overview.html", - copr=copr, - form=form) - - -@coprs_ns.route("///permissions/") -def copr_permissions(username, coprname): - query = coprs_logic.CoprsLogic.get(flask.g.user, username, coprname) - copr = query.first() - if not copr: - return page_not_found( - "Copr with name {0} does not exist.".format(coprname)) - - permissions = coprs_logic.CoprPermissionsLogic.get_for_copr( - flask.g.user, copr).all() - if flask.g.user: - user_perm = flask.g.user.permissions_for_copr(copr) - else: - user_perm = None - - permissions_applier_form = None - permissions_form = None - - # generate a proper form for displaying - if flask.g.user: - if flask.g.user.can_edit(copr): - permissions_form = forms.PermissionsFormFactory.create_form_cls( - permissions)() - else: - # https://github.com/ajford/flask-wtf/issues/58 - permissions_applier_form = \ - forms.PermissionsApplierFormFactory.create_form_cls( - user_perm)(formdata=None) - - return flask.render_template( - "coprs/detail/permissions.html", - copr=copr, - permissions_form=permissions_form, - permissions_applier_form=permissions_applier_form, - permissions=permissions, - current_user_permissions=user_perm) - - -@coprs_ns.route("///edit/") -@login_required -def copr_edit(username, coprname, form=None): - query = coprs_logic.CoprsLogic.get(flask.g.user, username, coprname) - copr = query.first() - - if not copr: - return page_not_found( - "Copr with name {0} does not exist.".format(coprname)) - - if not form: - form = forms.CoprFormFactory.create_form_cls( - copr.mock_chroots)(obj=copr) - - return flask.render_template("coprs/detail/edit.html", - copr=copr, - form=form) - - -@coprs_ns.route("///update/", methods=["POST"]) -@login_required -def copr_update(username, coprname): - form = forms.CoprFormFactory.create_form_cls()() - copr = coprs_logic.CoprsLogic.get(flask.g.user, username, coprname).first() - - if form.validate_on_submit(): - # we don"t change owner (yet) - copr.name = form.name.data - copr.repos = form.repos.data.replace("\n", " ") - copr.description = form.description.data - copr.instructions = form.instructions.data - coprs_logic.CoprChrootsLogic.update_from_names( - flask.g.user, copr, form.selected_chroots) - - try: - # form validation checks for duplicates - coprs_logic.CoprsLogic.update( - flask.g.user, copr, check_for_duplicates=False) - except (exceptions.ActionInProgressException, - exceptions.InsufficientRightsException) as e: - - flask.flash(str(e)) - db.session.rollback() - else: - flask.flash("Project was updated successfully.") - db.session.commit() - - return flask.redirect(flask.url_for("coprs_ns.copr_detail", - username=username, - coprname=copr.name)) - else: - return copr_edit(username, coprname, form) - - -@coprs_ns.route("///permissions_applier_change/", - methods=["POST"]) -@login_required -def copr_permissions_applier_change(username, coprname): - copr = coprs_logic.CoprsLogic.get(flask.g.user, username, coprname).first() - permission = coprs_logic.CoprPermissionsLogic.get( - flask.g.user, copr, flask.g.user).first() - applier_permissions_form = \ - forms.PermissionsApplierFormFactory.create_form_cls(permission)() - - if not copr: - return page_not_found( - "Project with name {0} does not exist.".format(coprname)) - - if copr.owner == flask.g.user: - flask.flash("Owner cannot request permissions for his own project.") - elif applier_permissions_form.validate_on_submit(): - # we rely on these to be 0 or 1 from form. TODO: abstract from that - new_builder = applier_permissions_form.copr_builder.data - new_admin = applier_permissions_form.copr_admin.data - coprs_logic.CoprPermissionsLogic.update_permissions_by_applier( - flask.g.user, copr, permission, new_builder, new_admin) - db.session.commit() - flask.flash( - "Successfuly updated permissions for project '{0}'." - .format(copr.name)) - - return flask.redirect(flask.url_for("coprs_ns.copr_detail", - username=copr.owner.name, - coprname=copr.name)) - - -@coprs_ns.route("///update_permissions/", methods=["POST"]) -@login_required -def copr_update_permissions(username, coprname): - query = coprs_logic.CoprsLogic.get(flask.g.user, username, coprname) - copr = query.first() - permissions = copr.copr_permissions - permissions_form = forms.PermissionsFormFactory.create_form_cls( - permissions)() - - if permissions_form.validate_on_submit(): - # we don't change owner (yet) - try: - # if admin is changing his permissions, his must be changed last - # so that we don't get InsufficientRightsException - permissions.sort( - cmp=lambda x, y: -1 if y.user_id == flask.g.user.id else 1) - for perm in permissions: - new_builder = permissions_form[ - "copr_builder_{0}".format(perm.user_id)].data - new_admin = permissions_form[ - "copr_admin_{0}".format(perm.user_id)].data - coprs_logic.CoprPermissionsLogic.update_permissions( - flask.g.user, copr, perm, new_builder, new_admin) - # for now, we don't check for actions here, as permissions operation - # don't collide with any actions - except exceptions.InsufficientRightsException as e: - db.session.rollback() - flask.flash(str(e)) - else: - db.session.commit() - flask.flash("Project permissions were updated successfully.") - - return flask.redirect(flask.url_for("coprs_ns.copr_detail", - username=copr.owner.name, - coprname=copr.name)) - - -@coprs_ns.route("///delete/", methods=["GET", "POST"]) -@login_required -def copr_delete(username, coprname): - form = forms.CoprDeleteForm() - copr = coprs_logic.CoprsLogic.get(flask.g.user, username, coprname).first() - - if form.validate_on_submit() and copr: - try: - coprs_logic.CoprsLogic.delete(flask.g.user, copr) - except (exceptions.ActionInProgressException, - exceptions.InsufficientRightsException) as e: - - db.session.rollback() - flask.flash(str(e)) - return flask.redirect(flask.url_for("coprs_ns.copr_detail", - username=username, - coprname=coprname)) - else: - db.session.commit() - flask.flash("Project was deleted successfully.") - return flask.redirect(flask.url_for("coprs_ns.coprs_by_owner", - username=username)) - else: - if copr: - return flask.render_template("coprs/detail/delete.html", - form=form, copr=copr) - else: - return page_not_found("Project {0}/{1} does not exist" - .format(username, coprname)) - - -@coprs_ns.route("///legal_flag/", methods=["POST"]) -@login_required -def copr_legal_flag(username, coprname): - form = forms.CoprLegalFlagForm() - copr = coprs_logic.CoprsLogic.get(flask.g.user, username, coprname).first() - - legal_flag = models.LegalFlag(raise_message=form.comment.data, - raised_on=int(time.time()), - copr=copr, - reporter=flask.g.user) - db.session.add(legal_flag) - db.session.commit() - - send_to = app.config["SEND_LEGAL_TO"] or ["root@localhost"] - hostname = platform.node() - navigate_to = "\nNavigate to http://{0}{1}".format( - hostname, flask.url_for("admin_ns.legal_flag")) - - contact = "\nContact on owner is: {0} <{1}>".format(username, - copr.owner.mail) - - reported_by = "\nReported by {0} <{1}>".format(flask.g.user.name, - flask.g.user.mail) - - try: - msg = MIMEText( - form.comment.data + navigate_to + contact + reported_by, "plain") - except UnicodeEncodeError: - msg = MIMEText(form.comment.data.encode( - "utf-8") + navigate_to + contact + reported_by, "plain", "utf-8") - - msg["Subject"] = "Legal flag raised on {0}".format(coprname) - msg["From"] = "root@{0}".format(hostname) - msg["To"] = ", ".join(send_to) - s = smtplib.SMTP("localhost") - s.sendmail("root@{0}".format(hostname), send_to, msg.as_string()) - s.quit() - - flask.flash("Admin was noticed about your report" - " and will investigate the project shortly.") - - return flask.redirect(flask.url_for("coprs_ns.copr_detail", - username=username, - coprname=coprname)) - - -@coprs_ns.route("///repo//") -def generate_repo_file(username, coprname, chroot): - """ Generate repo file for a given repo name. - Reponame = username-coprname """ - # This solution is used because flask splits off the last part after a - # dash, therefore user-re-po resolves to user-re/po instead of user/re-po - # FAS usernames may not contain dashes, so this construction is safe. - - reponame = "{0}-{1}".format(username, coprname) - - if "-" not in reponame: - return page_not_found( - "Bad repository name: {0}. Must be username-projectname" - .format(reponame)) - - copr = None - try: - # query.one() is used since it fetches all builds, unlike - # query.first(). - copr = coprs_logic.CoprsLogic.get(flask.g.user, username, coprname, - with_builds=True).one() - except sqlalchemy.orm.exc.NoResultFound: - return page_not_found( - "Project {0}/{1} does not exist".format(username, coprname)) - - try: - mock_chroot = coprs_logic.MockChrootsLogic.get_from_name(chroot).one() - except sqlalchemy.orm.exc.NoResultFound: - return page_not_found("Chroot {0} does not exist".format(chroot)) - except ValueError as e: - return page_not_found(str(e)) - - url = "" - for build in copr.builds: - if build.results: - url = build.results - break - - if not url: - return page_not_found( - "Repository not initialized: No finished builds in {0}/{1}." - .format(username, coprname)) - - response = flask.make_response(render_repo(copr, mock_chroot, url)) - response.mimetype = "text/plain" - response.headers["Content-Disposition"] = "filename={0}.repo".format( - reponame) - - return response - - -@coprs_ns.route("///monitor/") -def copr_build_monitor(username, coprname): - query = coprs_logic.CoprsLogic.get( - flask.g.user, username, coprname, with_mock_chroots=True) - form = forms.CoprLegalFlagForm() - try: - copr = query.one() - except sqlalchemy.orm.exc.NoResultFound: - return page_not_found( - "Copr with name {0} does not exist.".format(coprname)) - - builds_query = builds_logic.BuildsLogic.get_multiple( - flask.g.user, copr=copr) - builds = builds_query.order_by("-id").all() - - # please don"t waste time trying to decipher this - # the only reason why this is necessary is non-existent - # database design - # - # loop goes through builds trying to approximate - # per-package results based on previous builds - # - it can"t determine build results if build contains - # more than one package as this data is not available - - out = {} - build = None - chroots = set([chroot.name for chroot in copr.active_chroots]) - latest_build = None - - if builds: - latest_build = builds[0] - chroots.union([chroot.name for chroot in latest_build.build_chroots]) - - chroots = sorted(chroots) - - for build in builds: - chroot_results = {chroot.name: chroot.state - for chroot in build.build_chroots} - - build_results = [] - for chroot_name in chroots: - if chroot_name in chroot_results: - build_results.append((build.id, chroot_results[chroot_name])) - else: - build_results.append((build.id, None)) - - for pkg_url in build.pkgs.split(): - pkg = os.path.basename(pkg_url) - pkg_name = parse_package_name(pkg) - - if pkg_name in out: - continue - - out[pkg_name] = build_results - - return flask.render_template("coprs/detail/monitor.html", - copr=copr, - build=latest_build, - chroots=chroots, - packages=sorted(out.iteritems()), - form=form) diff --git a/coprs_frontend/coprs/views/misc.py b/coprs_frontend/coprs/views/misc.py deleted file mode 100644 index 4652ab4..0000000 --- a/coprs_frontend/coprs/views/misc.py +++ /dev/null @@ -1,153 +0,0 @@ -import base64 -import datetime -import functools - -import flask - -from coprs import app -from coprs import db -from coprs import helpers -from coprs import models -from coprs import oid - - -@app.before_request -def lookup_current_user(): - flask.g.user = None - if "openid" in flask.session: - flask.g.user = models.User.query.filter( - models.User.openid_name == flask.session["openid"]).first() - - -@app.errorhandler(404) -def page_not_found(message): - return flask.render_template("404.html", message=message), 404 - - -misc = flask.Blueprint("misc", __name__) - - -@misc.route("/login/", methods=["GET"]) -@oid.loginhandler -def login(): - if flask.g.user is not None: - return flask.redirect(oid.get_next_url()) - else: - return oid.try_login("https://id.fedoraproject.org/", - ask_for=["email", "timezone"]) - - -@oid.after_login -def create_or_login(resp): - flask.session["openid"] = resp.identity_url - fasusername = resp.identity_url.replace( - ".id.fedoraproject.org/", "").replace("http://", "") - - # kidding me.. or not - if fasusername and ((app.config["USE_ALLOWED_USERS"] - and fasusername in app.config["ALLOWED_USERS"]) - or not app.config["USE_ALLOWED_USERS"]): - - user = models.User.query.filter( - models.User.openid_name == resp.identity_url).first() - if not user: # create if not created already - expiration_date_token = datetime.date.today() + \ - datetime.timedelta( - days=flask.current_app.config["API_TOKEN_EXPIRATION"]) - - copr64 = base64.b64encode("copr") + "##" - user = models.User(openid_name=resp.identity_url, mail=resp.email, - timezone=resp.timezone, - api_login=copr64 + helpers.generate_api_token( - app.config["API_TOKEN_LENGTH"] - len(copr64)), - api_token=helpers.generate_api_token( - app.config["API_TOKEN_LENGTH"]), - api_token_expiration=expiration_date_token) - else: - user.mail = resp.email - user.timezone = resp.timezone - - db.session.add(user) - db.session.commit() - flask.flash(u"Welcome, {0}".format(user.name)) - flask.g.user = user - - if flask.request.url_root == oid.get_next_url(): - return flask.redirect(flask.url_for("coprs_ns.coprs_by_owner", - username=user.name)) - return flask.redirect(oid.get_next_url()) - else: - flask.flash("User '{0}' is not allowed".format(user.name)) - return flask.redirect(oid.get_next_url()) - - -@misc.route("/logout/") -def logout(): - flask.session.pop("openid", None) - flask.flash(u"You were signed out") - return flask.redirect(oid.get_next_url()) - - -def api_login_required(f): - @functools.wraps(f) - def decorated_function(*args, **kwargs): - token = None - username = None - if "Authorization" in flask.request.headers: - base64string = flask.request.headers["Authorization"] - base64string = base64string.split()[1].strip() - userstring = base64.b64decode(base64string) - (username, token) = userstring.split(":") - token_auth = False - if token and username: - user = models.User.query.filter( - models.User.api_login == username).first() - if (user and user.api_token == token and - user.api_token_expiration >= datetime.date.today()): - - token_auth = True - flask.g.user = user - if not token_auth: - output = {"output": "notok", "error": "Login invalid/expired"} - jsonout = flask.jsonify(output) - jsonout.status_code = 500 - return jsonout - return f(*args, **kwargs) - return decorated_function - - -def login_required(role=helpers.RoleEnum("user")): - def view_wrapper(f): - @functools.wraps(f) - def decorated_function(*args, **kwargs): - if flask.g.user is None: - return flask.redirect(flask.url_for("misc.login", - next=flask.request.url)) - - if role == helpers.RoleEnum("admin") and not flask.g.user.admin: - flask.flash("You are not allowed to access admin section.") - return flask.redirect(flask.url_for("coprs_ns.coprs_show")) - - return f(*args, **kwargs) - return decorated_function - # hack: if login_required is used without params, the "role" parameter - # is in fact the decorated function, so we need to return - # the wrapped function, not the wrapper - # proper solution would be to use login_required() with parentheses - # everywhere, even if they"re empty - TODO - if callable(role): - return view_wrapper(role) - else: - return view_wrapper - - -# backend authentication -def backend_authenticated(f): - @functools.wraps(f) - def decorated_function(*args, **kwargs): - auth = flask.request.authorization - if not auth or auth.password != app.config["BACKEND_PASSWORD"]: - return "You have to provide the correct password", 401 - - return f(*args, **kwargs) - return decorated_function diff --git a/coprs_frontend/coprs/whoosheers.py b/coprs_frontend/coprs/whoosheers.py deleted file mode 100644 index e4143d9..0000000 --- a/coprs_frontend/coprs/whoosheers.py +++ /dev/null @@ -1,52 +0,0 @@ -import whoosh - -from flask.ext.whooshee import AbstractWhoosheer - -from coprs import models -from coprs import whooshee - - -@whooshee.register_whoosheer -class CoprUserWhoosheer(AbstractWhoosheer): - schema = whoosh.fields.Schema( - copr_id=whoosh.fields.NUMERIC(stored=True, unique=True), - user_id=whoosh.fields.NUMERIC(stored=True), - username=whoosh.fields.TEXT(), - coprname=whoosh.fields.TEXT(), - description=whoosh.fields.TEXT(), - instructions=whoosh.fields.TEXT()) - - models = [models.Copr, models.User] - - @classmethod - def update_user(cls, writer, user): - # TODO: this is not needed now, as users can't change names, but may be - # needed later - pass - - @classmethod - def update_copr(cls, writer, copr): - writer.update_document(copr_id=copr.id, - user_id=copr.owner.id, - username=copr.owner.name, - coprname=copr.name, - description=copr.description, - instructions=copr.instructions) - - @classmethod - def insert_user(cls, writer, user): - # nothing, user doesn't have coprs yet - pass - - @classmethod - def insert_copr(cls, writer, copr): - writer.add_document(copr_id=copr.id, - user_id=copr.owner.id, - username=copr.owner.name, - coprname=copr.name, - description=copr.description, - instructions=copr.instructions) - - @classmethod - def delete_copr(cls, writer, copr): - writer.delete_by_term("copr_id", copr.id) diff --git a/coprs_frontend/manage.py b/coprs_frontend/manage.py deleted file mode 100755 index 3822d24..0000000 --- a/coprs_frontend/manage.py +++ /dev/null @@ -1,232 +0,0 @@ -#!/usr/bin/env python - -import argparse -import os -import subprocess - -import flask -from flask.ext.script import Manager, Command, Option, Group - -from coprs import app -from coprs import db -from coprs import exceptions -from coprs import models -from coprs.logic import coprs_logic - - -class TestCommand(Command): - - def run(self, test_args): - os.environ["COPRS_ENVIRON_UNITTEST"] = "1" - if not (("COPR_CONFIG" in os.environ) and os.environ["COPR_CONFIG"]): - os.environ["COPR_CONFIG"] = "/etc/copr/copr_unit_test.conf" - os.environ["PYTHONPATH"] = "." - return subprocess.call(["py.test"] + (test_args or [])) - - option_list = ( - Option("-a", - dest="test_args", - nargs=argparse.REMAINDER), - ) - - -class CreateSqliteFileCommand(Command): - - """ - Create the sqlite DB file (not the tables). - Used for alembic, "create_db" does this automatically. - """ - - def run(self): - if flask.current_app.config["SQLALCHEMY_DATABASE_URI"].startswith("sqlite"): - # strip sqlite:/// - datadir_name = os.path.dirname( - flask.current_app.config["SQLALCHEMY_DATABASE_URI"][10:]) - if not os.path.exists(datadir_name): - os.makedirs(datadir_name) - - -class CreateDBCommand(Command): - - """ - Create the DB schema - """ - - def run(self, alembic_ini=None): - CreateSqliteFileCommand().run() - db.create_all() - - # load the Alembic configuration and generate the - # version table, "stamping" it with the most recent rev: - from alembic.config import Config - from alembic import command - alembic_cfg = Config(alembic_ini) - command.stamp(alembic_cfg, "head") - - option_list = ( - Option("--alembic", - "-f", - dest="alembic_ini", - help="Path to the alembic configuration file (alembic.ini)", - required=True), - ) - - -class DropDBCommand(Command): - - """ - Delete DB - """ - - def run(self): - db.drop_all() - - -class ChrootCommand(Command): - - def print_invalid_format(self, chroot_name): - print( - "{0} - invalid chroot format, must be '{release}-{version}-{arch}'." - .format(chroot_name)) - - def print_already_exists(self, chroot_name): - print("{0} - already exists.".format(chroot_name)) - - def print_doesnt_exist(self, chroot_name): - print("{0} - chroot doesn\"t exist.".format(chroot_name)) - - option_list = ( - Option("chroot_names", - help="Chroot name, e.g. fedora-18-x86_64.", - nargs="+"), - ) - - -class CreateChrootCommand(ChrootCommand): - - "Creates a mock chroot in DB" - - def run(self, chroot_names): - for chroot_name in chroot_names: - try: - coprs_logic.MockChrootsLogic.add(None, chroot_name) - db.session.commit() - except exceptions.MalformedArgumentException: - self.print_invalid_format(chroot_name) - except exceptions.DuplicateException: - self.print_already_exists(chroot_name) - - -class AlterChrootCommand(ChrootCommand): - - "Activates or deactivates a chroot" - - def run(self, chroot_names, action): - activate = (action == "activate") - for chroot_name in chroot_names: - try: - coprs_logic.MockChrootsLogic.edit_by_name( - None, chroot_name, activate) - db.session.commit() - except exceptions.MalformedArgumentException: - self.print_invalid_format(chroot_name) - except exceptions.NotFoundException: - self.print_doesnt_exist(chroot_name) - - option_list = ChrootCommand.option_list + ( - Option("--action", - "-a", - dest="action", - help="Action to take - currently activate or deactivate", - choices=["activate", "deactivate"], - required=True), - ) - - -class DropChrootCommand(ChrootCommand): - - "Activates or deactivates a chroot" - - def run(self, chroot_names): - for chroot_name in chroot_names: - try: - coprs_logic.MockChrootsLogic.delete_by_name(None, chroot_name) - db.session.commit() - except exceptions.MalformedArgumentException: - self.print_invalid_format(chroot_name) - except exceptions.NotFoundException: - self.print_doesnt_exist(chroot_name) - - -class DisplayChrootsCommand(Command): - - "Displays current mock chroots" - - def run(self, active_only): - for ch in coprs_logic.MockChrootsLogic.get_multiple( - None, active_only=active_only).all(): - - print(ch.name) - - option_list = ( - Option("--active-only", - "-a", - dest="active_only", - help="Display only active chroots", - required=False, - action="store_true", - default=False), - ) - - -class AlterUserCommand(Command): - - def run(self, name, **kwargs): - user = models.User.query.filter( - models.User.openid_name == models.User.openidize_name(name)).first() - if not user: - print("No user named {0}.".format(name)) - return - - if kwargs["admin"]: - user.admin = True - if kwargs["no_admin"]: - user.admin = False - if kwargs["proven"]: - user.proven = True - if kwargs["no_proven"]: - user.proven = False - - db.session.commit() - - option_list = ( - Option("name"), - Group( - Option("--admin", - action="store_true"), - Option("--no-admin", - action="store_true"), - exclusive=True - ), - Group( - Option("--proven", - action="store_true"), - Option("--no-proven", - action="store_true"), - exclusive=True - ) - ) - -manager = Manager(app) -manager.add_command("test", TestCommand()) -manager.add_command("create_sqlite_file", CreateSqliteFileCommand()) -manager.add_command("create_db", CreateDBCommand()) -manager.add_command("drop_db", DropDBCommand()) -manager.add_command("create_chroot", CreateChrootCommand()) -manager.add_command("alter_chroot", AlterChrootCommand()) -manager.add_command("display_chroots", DisplayChrootsCommand()) -manager.add_command("drop_chroot", DropChrootCommand()) -manager.add_command("alter_user", AlterUserCommand()) - -if __name__ == "__main__": - manager.run() diff --git a/coprs_frontend/tests/__init__.py b/coprs_frontend/tests/__init__.py deleted file mode 100644 index e69de29..0000000 --- a/coprs_frontend/tests/__init__.py +++ /dev/null diff --git a/coprs_frontend/tests/coprs_test_case.py b/coprs_frontend/tests/coprs_test_case.py deleted file mode 100644 index 313cef7..0000000 --- a/coprs_frontend/tests/coprs_test_case.py +++ /dev/null @@ -1,224 +0,0 @@ -import base64 -import os -import time -from functools import wraps - -import pytest -import decorator - -import coprs - -from coprs import helpers -from coprs import models - - -class CoprsTestCase(object): - - def setup_method(self, method): - self.tc = coprs.app.test_client() - self.app = coprs.app - self.app.testing = True - self.db = coprs.db - self.db.session = self.db.create_scoped_session() - self.models = models - self.helpers = helpers - self.backend_passwd = coprs.app.config["BACKEND_PASSWORD"] - # create datadir if it doesn't exist - datadir = os.path.commonprefix( - [self.app.config["DATABASE"], self.app.config["OPENID_STORE"]]) - if not os.path.exists(datadir): - os.makedirs(datadir) - coprs.db.create_all() - self.db.session.commit() - - def teardown_method(self, method): - # delete just data, not the tables - for tbl in reversed(self.db.metadata.sorted_tables): - self.db.engine.execute(tbl.delete()) - - @property - def auth_header(self): - return {"Authorization": "Basic " + - base64.b64encode("doesntmatter:{0}".format(self.backend_passwd))} - - @pytest.fixture - def f_db(self): - self.db.session.commit() - - @pytest.fixture - def f_users(self): - self.u1 = models.User( - openid_name=u"http://user1.id.fedoraproject.org/", - proven=False, - admin=True, - mail="user1@foo.bar") - - self.u2 = models.User( - openid_name=u"http://user2.id.fedoraproject.org/", - proven=False, - mail="user2@spam.foo") - - self.u3 = models.User( - openid_name=u"http://user3.id.fedoraproject.org/", - proven=False, - mail="baz@bar.bar") - - self.db.session.add_all([self.u1, self.u2, self.u3]) - - @pytest.fixture - def f_coprs(self): - self.c1 = models.Copr(name=u"foocopr", owner=self.u1) - self.c2 = models.Copr(name=u"foocopr", owner=self.u2) - self.c3 = models.Copr(name=u"barcopr", owner=self.u2) - - self.db.session.add_all([self.c1, self.c2, self.c3]) - - @pytest.fixture - def f_mock_chroots(self): - self.mc1 = models.MockChroot( - os_release="fedora", os_version="18", arch="x86_64", is_active=True) - self.mc2 = models.MockChroot( - os_release="fedora", os_version="17", arch="x86_64", is_active=True) - self.mc3 = models.MockChroot( - os_release="fedora", os_version="17", arch="i386", is_active=True) - self.mc4 = models.MockChroot( - os_release="fedora", os_version="rawhide", arch="i386", is_active=True) - - # only bind to coprs if the test has used the f_coprs fixture - if hasattr(self, "c1"): - cc1 = models.CoprChroot() - cc1.mock_chroot = self.mc1 - # c1 foocopr with fedora-18-x86_64 - self.c1.copr_chroots.append(cc1) - - cc2 = models.CoprChroot() - cc2.mock_chroot = self.mc2 - cc3 = models.CoprChroot() - cc3.mock_chroot = self.mc3 - # c2 foocopr with fedora-17-i386 fedora-17-x86_64 - self.c2.copr_chroots.append(cc2) - self.c2.copr_chroots.append(cc3) - - cc4 = models.CoprChroot() - cc4.mock_chroot = self.mc4 - # c3 barcopr with fedora-rawhide-i386 - self.c3.copr_chroots.append(cc4) - self.db.session.add_all([cc1, cc2, cc3, cc4]) - - self.db.session.add_all([self.mc1, self.mc2, self.mc3, self.mc4]) - - @pytest.fixture - def f_builds(self): - self.b1 = models.Build( - copr=self.c1, user=self.u1, submitted_on=50, started_on=139086644000) - self.b2 = models.Build( - copr=self.c1, user=self.u2, submitted_on=10, ended_on=139086644000) - self.b3 = models.Build( - copr=self.c2, user=self.u2, submitted_on=10) - self.b4 = models.Build( - copr=self.c2, user=self.u2, submitted_on=100) - - for build in [self.b1, self.b2, self.b3, self.b4]: - self.db.session.add(build) - - for chroot in build.copr.active_chroots: - buildchroot = models.BuildChroot( - build=build, - mock_chroot=chroot) - - self.db.session.add(buildchroot) - - self.db.session.add_all([self.b1, self.b2, self.b3, self.b4]) - - @pytest.fixture - def f_copr_permissions(self): - self.cp1 = models.CoprPermission( - copr=self.c2, - user=self.u1, - copr_builder=helpers.PermissionEnum("approved"), - copr_admin=helpers.PermissionEnum("nothing")) - - self.cp2 = models.CoprPermission( - copr=self.c3, - user=self.u3, - copr_builder=helpers.PermissionEnum("nothing"), - copr_admin=helpers.PermissionEnum("nothing")) - - self.cp3 = models.CoprPermission( - copr=self.c3, - user=self.u1, - copr_builder=helpers.PermissionEnum("request"), - copr_admin=helpers.PermissionEnum("approved")) - - self.db.session.add_all([self.cp1, self.cp2, self.cp3]) - - @pytest.fixture - def f_actions(self): - # if using actions, we need to flush coprs into db, so that we can get - # their ids - self.f_db() - self.a1 = models.Action(action_type=helpers.ActionTypeEnum("rename"), - object_type="copr", - object_id=self.c1.id, - old_value="{0}/{1}".format( - self.c1.owner.name, self.c1.name), - new_value="{0}/new_name".format( - self.c1.owner.name), - created_on=int(time.time())) - self.a2 = models.Action(action_type=helpers.ActionTypeEnum("rename"), - object_type="copr", - object_id=self.c2.id, - old_value="{0}/{1}".format( - self.c2.owner.name, self.c2.name), - new_value="{0}/new_name2".format( - self.c2.owner.name), - created_on=int(time.time())) - self.a3 = models.Action(action_type=helpers.ActionTypeEnum("delete"), - object_type="copr", - object_id=100, - old_value="asd/qwe", - new_value=None, - result=helpers.BackendResultEnum("success"), - created_on=int(time.time())) - self.db.session.add_all([self.a1, self.a2, self.a3]) - - -class TransactionDecorator(object): - - """ - This is decorator as a class. - - Its purpose is to replace repetative lines of 'with' statements - in test's functions. Everytime you find your self writing test function - which uses following 'with's construct: - - with self.tc as test_client: - with c.session_transaction() as session: - session['openid'] = self.u.openid_name - - where 'u' stands for any user from 'f_users' fixture, use this to decorate - your test function: - - @TransactionDecorator('u') - def test_function_without_with_statements(self, f_users): - # write code as you were in with 'self.tc as test_client' indent - # you can also access object 'test_client' through 'self.test_client' - - where decorator parameter ''u'' stands for string representation of any - user from 'f_users' fixture from which you wish to store 'openid_name'. - Please note that you **must** include 'f_users' fixture in decorated - function parameters. - - """ - - def __init__(self, user): - self.user = user - - def __call__(self, fn): - @wraps(fn) - def wrapper(fn, fn_self, *args): - with fn_self.tc as fn_self.test_client: - with fn_self.test_client.session_transaction() as session: - session["openid"] = getattr(fn_self, self.user).openid_name - return fn(fn_self, *args) - return decorator.decorator(wrapper, fn) diff --git a/coprs_frontend/tests/test_helpers.py b/coprs_frontend/tests/test_helpers.py deleted file mode 100644 index 6851bf6..0000000 --- a/coprs_frontend/tests/test_helpers.py +++ /dev/null @@ -1,27 +0,0 @@ -from coprs.helpers import parse_package_name - -from tests.coprs_test_case import CoprsTestCase - - -class TestHelpers(CoprsTestCase): - - def test_guess_package_name(self): - EXP = { - 'wat-1.2.rpm': 'wat', - 'will-crash-0.5-2.fc20.src.rpm': 'will-crash', - 'will-crash-0.5-2.fc20.src': 'will-crash', - 'will-crash-0.5-2.fc20': 'will-crash', - 'will-crash-0.5-2': 'will-crash', - 'will-crash-0.5-2.rpm': 'will-crash', - 'will-crash-0.5-2.src.rpm': 'will-crash', - 'will-crash': 'will-crash', - 'pkgname7.src.rpm': 'pkgname7', - 'copr-frontend-1.14-1.git.65.9ba5393.fc20.noarch': 'copr-frontend', - 'noversion.fc20.src.rpm': 'noversion', - 'nothing': 'nothing', - 'ruby193': 'ruby193', - 'xorg-x11-fonts-ISO8859-1-75dpi-7.1-2.1.el5.noarch.rpm': 'xorg-x11-fonts-ISO8859-1-75dpi', - } - - for pkg, expected in EXP.iteritems(): - assert parse_package_name(pkg) == expected diff --git a/coprs_frontend/tests/test_logic/test_builds_logic.py b/coprs_frontend/tests/test_logic/test_builds_logic.py deleted file mode 100644 index 9871664..0000000 --- a/coprs_frontend/tests/test_logic/test_builds_logic.py +++ /dev/null @@ -1,40 +0,0 @@ -import pytest - -from coprs.exceptions import ActionInProgressException -from coprs.logic.builds_logic import BuildsLogic - -from tests.coprs_test_case import CoprsTestCase - - -class TestBuildsLogic(CoprsTestCase): - - def test_add_only_adds_active_chroots(self, f_users, f_coprs, f_builds, - f_mock_chroots, f_db): - - self.mc2.is_active = False - self.db.session.commit() - b = BuildsLogic.add(self.u2, "blah blah", self.c2) - self.db.session.commit() - assert b.chroots[0].name == self.mc3.name - - def test_add_raises_if_copr_has_unfinished_actions(self, f_users, f_coprs, - f_actions, f_db): - - with pytest.raises(ActionInProgressException): - b = BuildsLogic.add(self.u1, "blah blah", self.c1) - self.db.session.rollback() - - def test_add_assigns_params_correctly(self, f_users, f_coprs, - f_mock_chroots, f_db): - - params = dict( - user=self.u1, - pkgs="blah blah", - copr=self.c1, - repos="repos", - memory_reqs=3000, - timeout=5000) - - b = BuildsLogic.add(**params) - for k, v in params.items(): - assert getattr(b, k) == v diff --git a/coprs_frontend/tests/test_logic/test_coprs_logic.py b/coprs_frontend/tests/test_logic/test_coprs_logic.py deleted file mode 100644 index 39f600a..0000000 --- a/coprs_frontend/tests/test_logic/test_coprs_logic.py +++ /dev/null @@ -1,30 +0,0 @@ -import pytest - -from coprs.exceptions import ActionInProgressException -from coprs.helpers import ActionTypeEnum -from coprs.logic.coprs_logic import CoprsLogic - -from tests.coprs_test_case import CoprsTestCase - - -class TestCoprsLogic(CoprsTestCase): - - def test_update_raises_if_copr_has_unfinished_actions(self, f_users, - f_coprs, f_actions, - f_db): - self.c1.name = "foo" - with pytest.raises(ActionInProgressException): - CoprsLogic.update(self.u1, self.c1) - self.db.session.rollback() - - def test_legal_flag_doesnt_block_copr_functionality(self, f_users, - f_coprs, f_db): - self.db.session.add(self.models.Action( - object_type="copr", - object_id=self.c1.id, - action_type=ActionTypeEnum("legal-flag"))) - - self.db.session.commit() - # test will fail if this raises exception - CoprsLogic.raise_if_unfinished_blocking_action( - None, self.c1, "ha, failed") diff --git a/coprs_frontend/tests/test_views/test_admin/test_admin_general.py b/coprs_frontend/tests/test_views/test_admin/test_admin_general.py deleted file mode 100644 index 60eba07..0000000 --- a/coprs_frontend/tests/test_views/test_admin/test_admin_general.py +++ /dev/null @@ -1,23 +0,0 @@ -from tests.coprs_test_case import CoprsTestCase - - -class TestAdminLogin(CoprsTestCase): - # TODO: test on something better then page title - maybe see rendered - # templates? - text_to_check = "Coprs - Admin" - - def test_nonadmin_cant_login(self, f_users, f_db): - with self.tc as c: - with c.session_transaction() as s: - s["openid"] = self.u2.openid_name - - r = c.get("/admin/", follow_redirects=True) - assert self.text_to_check not in r.data - - def test_admin_can_login(self, f_users, f_db): - with self.tc as c: - with c.session_transaction() as s: - s["openid"] = self.u1.openid_name - - r = c.get("/admin/", follow_redirects=True) - assert self.text_to_check in r.data diff --git a/coprs_frontend/tests/test_views/test_backend_ns/test_backend_general.py b/coprs_frontend/tests/test_views/test_backend_ns/test_backend_general.py deleted file mode 100644 index c46d5b3..0000000 --- a/coprs_frontend/tests/test_views/test_backend_ns/test_backend_general.py +++ /dev/null @@ -1,242 +0,0 @@ -import json - -from coprs.signals import build_finished -from tests.coprs_test_case import CoprsTestCase - - -class TestWaitingBuilds(CoprsTestCase): - - def test_no_waiting_builds(self): - assert '"builds": []' in self.tc.get( - "/backend/waiting/", headers=self.auth_header).data - - def test_waiting_build_only_lists_not_started_or_ended( - self, f_users, f_coprs, f_mock_chroots, f_builds, f_db): - - r = self.tc.get("/backend/waiting/", headers=self.auth_header) - assert len(json.loads(r.data)["builds"]) == 4 - - -# status = 0 # failure -# status = 1 # succeeded -class TestUpdateBuilds(CoprsTestCase): - data1 = """ -{ - "builds":[ - { - "id": 1, - "copr_id": 2, - "results": "http://server/results/foo/bar/", - "started_on": 139086644000 - } - ] -}""" - - data2 = """ -{ - "builds":[ - { - "id": 1, - "copr_id": 2, - "status": 1, - "chroot": "fedora-18-x86_64", - "ended_on": 139086644000 - } - ] -}""" - - data3 = """ -{ - "builds":[ - { - "id": 1, - "copr_id": 2, - "started_on": 139086644000 - }, - { - "id": 2, - "copr_id": 1, - "status": 0, - "chroot": "fedora-18-x86_64", - "results": "http://server/results/foo/bar/", - "ended_on": 139086644000 - }, - { - "id": 123321, - "copr_id": 1, - "status": 0, - "ended_on": 139086644000 - }, - { - "id": 1234321, - "copr_id": 2, - "results": "http://server/results/foo/bar/", - "started_on": 139086644000 - } - ] -}""" - - def test_updating_requires_password(self, f_users, f_coprs, f_builds, f_db): - r = self.tc.post("/backend/update/", - content_type="application/json", - data="") - assert "You have to provide the correct password" in r.data - - def test_update_build_started(self, f_users, f_coprs, f_builds, f_db): - self.b1.started_on = None - self.db.session.add(self.b1) - self.db.session.commit() - - r = self.tc.post("/backend/update/", - content_type="application/json", - headers=self.auth_header, - data=self.data1) - assert json.loads(r.data)["updated_builds_ids"] == [1] - assert json.loads(r.data)["non_existing_builds_ids"] == [] - - updated = self.models.Build.query.filter( - self.models.Build.id == 1).first() - assert updated.results == "http://server/results/foo/bar/" - assert updated.started_on == 139086644000 - - def test_update_build_ended(self, f_users, f_coprs, f_mock_chroots, - f_builds, f_db): - - r = self.tc.post("/backend/update/", - content_type="application/json", - headers=self.auth_header, - data=self.data2) - assert json.loads(r.data)["updated_builds_ids"] == [1] - assert json.loads(r.data)["non_existing_builds_ids"] == [] - - updated = self.models.Build.query.filter( - self.models.Build.id == 1).first() - assert updated.status == 1 - assert updated.ended_on == 139086644000 - - def test_update_more_existent_and_non_existent_builds( - self, f_users, f_coprs, f_mock_chroots, f_builds, f_db): - - self.b1.started_on = None - self.db.session.add(self.b1) - self.db.session.commit() - - r = self.tc.post("/backend/update/", - content_type="application/json", - headers=self.auth_header, - data=self.data3) - - assert sorted(json.loads(r.data)["updated_builds_ids"]) == [1, 2] - assert sorted(json.loads(r.data)["non_existing_builds_ids"]) == [ - 123321, 1234321] - - started = self.models.Build.query.filter( - self.models.Build.id == 1).first() - assert started.started_on == 139086644000 - - ended = self.models.Build.query.filter( - self.models.Build.id == 2).first() - assert ended.status == 0 - assert ended.results == "http://server/results/foo/bar/" - assert ended.ended_on == 139086644000 - - def test_build_ended_emmits_signal(self, f_users, f_coprs, f_builds, f_db): - # TODO: this should probably be mocked... - signals_received = [] - - def test_receiver(sender, **kwargs): - signals_received.append(kwargs["build"]) - build_finished.connect(test_receiver) - self.tc.post("/backend/update/", - content_type="application/json", - headers=self.auth_header, - data=self.data3) - assert len(signals_received) == 1 - self.db.session.add(self.b2) - assert signals_received[0].id == 2 - - -class TestWaitingActions(CoprsTestCase): - - def test_no_waiting_actions(self): - assert '"actions": []' in self.tc.get( - "/backend/waiting/", headers=self.auth_header).data - - def test_waiting_actions_only_lists_not_started_or_ended( - self, f_users, f_coprs, f_actions, f_db): - - r = self.tc.get("/backend/waiting/", headers=self.auth_header) - assert len(json.loads(r.data)["actions"]) == 2 - - -class TestUpdateActions(CoprsTestCase): - data1 = """ -{ - "actions":[ - { - "id": 1, - "result": 1, - "message": "no problem", - "ended_on": 139086644000 - } - ] -}""" - data2 = """ -{ - "actions":[ - { - "id": 1, - "result": 1, - "message": null, - "ended_on": 139086644000 - }, - { - "id": 2, - "result": 2, - "message": "problem!", - "ended_on": 139086644000 - }, - { - "id": 100, - "result": 123, - "message": "wheeeee!", - "ended_on": 139086644000 - } - ] -}""" - - def test_update_one_action(self, f_users, f_coprs, f_actions, f_db): - r = self.tc.post("/backend/update/", - content_type="application/json", - headers=self.auth_header, - data=self.data1) - assert json.loads(r.data)["updated_actions_ids"] == [1] - assert json.loads(r.data)["non_existing_actions_ids"] == [] - - updated = self.models.Action.query.filter( - self.models.Action.id == 1).first() - assert updated.result == 1 - assert updated.message == "no problem" - assert updated.ended_on == 139086644000 - - def test_update_more_existent_and_non_existent_builds(self, f_users, - f_coprs, f_actions, - f_db): - r = self.tc.post("/backend/update/", - content_type="application/json", - headers=self.auth_header, - data=self.data2) - assert sorted(json.loads(r.data)["updated_actions_ids"]) == [1, 2] - assert json.loads(r.data)["non_existing_actions_ids"] == [100] - - updated = self.models.Action.query.filter( - self.models.Action.id == 1).first() - assert updated.result == 1 - assert updated.message is None - assert updated.ended_on == 139086644000 - - updated2 = self.models.Action.query.filter( - self.models.Action.id == 2).first() - assert updated2.result == 2 - assert updated2.message == "problem!" - assert updated2.ended_on == 139086644000 diff --git a/coprs_frontend/tests/test_views/test_coprs_ns/__init__.py b/coprs_frontend/tests/test_views/test_coprs_ns/__init__.py deleted file mode 100644 index e69de29..0000000 --- a/coprs_frontend/tests/test_views/test_coprs_ns/__init__.py +++ /dev/null diff --git a/coprs_frontend/tests/test_views/test_coprs_ns/test_coprs_builds.py b/coprs_frontend/tests/test_views/test_coprs_ns/test_coprs_builds.py deleted file mode 100644 index c88799c..0000000 --- a/coprs_frontend/tests/test_views/test_coprs_ns/test_coprs_builds.py +++ /dev/null @@ -1,181 +0,0 @@ -from tests.coprs_test_case import CoprsTestCase, TransactionDecorator - - -class TestCoprShowBuilds(CoprsTestCase): - - def test_copr_show_builds(self, f_users, f_coprs, f_mock_chroots, - f_builds, f_db): - - r = self.tc.get( - "/coprs/{0}/{1}/builds/".format(self.u2.name, self.c2.name)) - assert r.data.count('') == 3 - - -class TestCoprsOwned(CoprsTestCase): - - @TransactionDecorator("u3") - def test_owned_none(self, f_users, f_coprs, f_db): - self.db.session.add(self.u3) - r = self.test_client.get("/coprs/{0}/".format(self.u3.name)) - assert "No projects..." in r.data - - @TransactionDecorator("u1") - def test_owned_one(self, f_users, f_coprs, f_db): - self.db.session.add(self.u1) - r = self.test_client.get("/coprs/{0}/".format(self.u1.name)) - assert r.data.count('
') == 1 - - -class TestCoprsAllowed(CoprsTestCase): - - @TransactionDecorator("u3") - def test_allowed_none(self, f_users, f_coprs, f_copr_permissions, f_db): - self.db.session.add(self.u3) - r = self.test_client.get("/coprs/{0}/allowed/".format(self.u3.name)) - assert "No projects..." in r.data - - @TransactionDecorator("u1") - def test_allowed_one(self, f_users, f_coprs, f_copr_permissions, f_db): - self.db.session.add(self.u1) - r = self.test_client.get("/coprs/{0}/allowed/".format(self.u1.name)) - assert r.data.count('
') == 1 - - -class TestCoprNew(CoprsTestCase): - success_string = "New project was successfully created" - - @TransactionDecorator("u1") - def test_copr_new_normal(self, f_users, f_mock_chroots, f_db): - r = self.test_client.post( - "/coprs/{0}/new/".format(self.u1.name), - data={"name": "foo", - "fedora-rawhide-i386": "y", - "arches": ["i386"]}, - follow_redirects=True) - - assert self.models.Copr.query.filter( - self.models.Copr.name == "foo").first() - assert self.success_string in r.data - - # make sure no initial build was submitted - assert self.models.Build.query.first() is None - - @TransactionDecorator("u1") - def test_copr_new_emits_signal(self, f_users, f_mock_chroots, f_db): - # TODO: this should probably be mocked... - signals_received = [] - - def test_receiver(sender, **kwargs): - signals_received.append(kwargs["copr"]) - copr_created.connect(test_receiver) - self.test_client.post( - "/coprs/{0}/new/".format(self.u1.name), - data={"name": "foo", - "fedora-rawhide-i386": "y", - "arches": ["i386"]}, - follow_redirects=True) - - assert len(signals_received) == 1 - assert signals_received[0].name == "foo" - - @TransactionDecorator("u3") - def test_copr_new_exists_for_another_user(self, f_users, f_coprs, - f_mock_chroots, f_db): - - self.db.session.add(self.c1) - foocoprs = len(self.models.Copr.query.filter( - self.models.Copr.name == self.c1.name).all()) - assert foocoprs > 0 - - r = self.test_client.post( - "/coprs/{0}/new/".format(self.u3.name), - data={"name": self.c1.name, - "fedora-rawhide-i386": "y"}, - follow_redirects=True) - - self.db.session.add(self.c1) - - assert len(self.models.Copr.query.filter( - self.models.Copr.name == self.c1.name).all()) == foocoprs + 1 - assert self.success_string in r.data - - @TransactionDecorator("u1") - def test_copr_new_exists_for_this_user(self, f_users, f_coprs, - f_mock_chroots, f_db): - self.db.session.add(self.c1) - foocoprs = len(self.models.Copr.query.filter( - self.models.Copr.name == self.c1.name).all()) - assert foocoprs > 0 - - r = self.test_client.post( - "/coprs/{0}/new/".format(self.u1.name), - data={"name": self.c1.name, - "fedora-rawhide-i386": "y"}, - follow_redirects=True) - - self.db.session.add(self.c1) - assert len(self.models.Copr.query.filter( - self.models.Copr.name == self.c1.name).all()) == foocoprs - assert "You already have project named" in r.data - - @TransactionDecorator("u1") - def test_copr_new_with_initial_pkgs(self, f_users, f_mock_chroots, f_db): - r = self.test_client.post("/coprs/{0}/new/".format(self.u1.name), - data={"name": "foo", - "fedora-rawhide-i386": "y", - "initial_pkgs": ["http://f", - "http://b"]}, - follow_redirects=True) - - copr = self.models.Copr.query.filter( - self.models.Copr.name == "foo").first() - assert copr - assert self.success_string in r.data - - assert self.models.Build.query.first().copr == copr - assert copr.build_count == 1 - assert "Initial packages were successfully submitted" in r.data - - @TransactionDecorator("u1") - def test_copr_new_is_allowed_even_if_deleted_has_same_name( - self, f_users, f_coprs, f_mock_chroots, f_db): - - self.db.session.add(self.c1) - self.c1.deleted = True - self.c1.owner = self.u1 - self.db.session.commit() - - self.db.session.add(self.c1) - r = self.test_client.post("/coprs/{0}/new/".format(self.u1.name), - data={"name": self.c1.name, - "fedora-rawhide-i386": "y", - "arches": ["i386"]}, - follow_redirects=True) - - self.c1 = self.db.session.merge(self.c1) - self.u1 = self.db.session.merge(self.u1) - assert len(self.models.Copr.query.filter(self.models.Copr.name == - self.c1.name) - .filter(self.models.Copr.owner == self.u1) - .all()) == 2 - assert self.success_string in r.data - - -class TestCoprDetail(CoprsTestCase): - - def test_copr_detail_not_found(self): - r = self.tc.get("/coprs/foo/bar/") - assert r.status_code == 404 - - def test_copr_detail_normal(self, f_users, f_coprs, f_db): - r = self.tc.get("/coprs/{0}/{1}/".format(self.u1.name, self.c1.name)) - assert r.status_code == 200 - assert self.c1.name in r.data - - def test_copr_detail_contains_builds(self, f_users, f_coprs, - f_mock_chroots, f_builds, f_db): - r = self.tc.get( - "/coprs/{0}/{1}/builds/".format(self.u1.name, self.c1.name)) - assert r.data.count('{0}'.format(self.u3.name) in r.data - assert '{0}'.format(self.u1.name) in r.data - - @TransactionDecorator("u1") - def test_copr_detail_allows_asking_for_permissions(self, f_users, f_coprs, - f_copr_permissions, f_db): - - self.db.session.add_all([self.u2, self.c2]) - r = self.test_client.get( - "/coprs/{0}/{1}/permissions/".format(self.u2.name, self.c2.name)) - # u1 is approved builder, check for that - assert "/permissions_applier_change/" in r.data - - @TransactionDecorator("u2") - def test_copr_detail_doesnt_allow_owner_to_ask_for_permissions( - self, f_users, f_coprs, f_db): - - self.db.session.add_all([self.u2, self.c2]) - r = self.test_client.get( - "/coprs/{0}/{1}/permissions/".format(self.u2.name, self.c2.name)) - assert "/permissions_applier_change/" not in r.data - - @TransactionDecorator("u2") - def test_detail_has_correct_permissions_form(self, f_users, f_coprs, - f_copr_permissions, f_db): - - self.db.session.add_all([self.u2, self.c3]) - r = self.test_client.get( - "/coprs/{0}/{1}/permissions/".format(self.u2.name, self.c3.name)) - - assert r.data.count("nothing") == 2 - assert '' in r.data - - def test_copr_detail_doesnt_show_cancel_build_for_anonymous(self, f_users, f_coprs, f_builds, f_db): - r = self.tc.get("/coprs/{0}/{1}/".format(self.u2.name, self.c2.name)) - assert "/cancel_build/" not in r.data - - @TransactionDecorator("u1") - def test_copr_detail_doesnt_allow_non_submitter_to_cancel_build( - self, f_users, f_coprs, f_mock_chroots, f_builds, f_db): - - self.db.session.add_all([self.u2, self.c2]) - r = self.test_client.get( - "/coprs/{0}/{1}/builds/".format(self.u2.name, self.c2.name)) - assert "/cancel_build/" not in r.data - - @TransactionDecorator("u2") - def test_copr_detail_allows_submitter_to_cancel_build( - self, f_users, f_coprs, f_mock_chroots, f_builds, f_db): - - self.db.session.add_all([self.u2, self.c2]) - r = self.test_client.get( - "/coprs/{0}/{1}/builds/".format(self.u2.name, self.c2.name)) - assert "/cancel_build/" in r.data - - -class TestCoprEdit(CoprsTestCase): - - @TransactionDecorator("u1") - def test_edit_prefills_id(self, f_users, f_coprs, f_db): - self.db.session.add_all([self.u1, self.c1]) - r = self.test_client.get( - "/coprs/{0}/{1}/edit/".format(self.u1.name, self.c1.name)) - # TODO: use some kind of html parsing library to look - # for the hidden input, this ties us - # to the precise format of the tag - assert ('' - .format(self.c1.id) in r.data) - - -class TestCoprUpdate(CoprsTestCase): - - @TransactionDecorator("u1") - def test_update_no_changes(self, f_users, f_coprs, f_mock_chroots, f_db): - self.db.session.add_all([self.u1, self.c1]) - r = self.test_client.post("/coprs/{0}/{1}/update/" - .format(self.u1.name, self.c1.name), - data={"name": self.c1.name, - "fedora-18-x86_64": "y", - "id": self.c1.id}, - follow_redirects=True) - - assert "Project was updated successfully" in r.data - - @TransactionDecorator("u1") - def test_copr_admin_can_update(self, f_users, f_coprs, - f_copr_permissions, f_mock_chroots, f_db): - - self.db.session.add_all([self.u2, self.c3]) - r = self.test_client.post("/coprs/{0}/{1}/update/" - .format(self.u2.name, self.c3.name), - data={"name": self.c3.name, - "fedora-rawhide-i386": "y", - "id": self.c3.id}, - follow_redirects=True) - - assert "Project was updated successfully" in r.data - - @TransactionDecorator("u1") - def test_update_multiple_chroots(self, f_users, f_coprs, - f_copr_permissions, f_mock_chroots, f_db): - - self.db.session.add_all( - [self.u1, self.c1, self.mc1, self.mc2, self.mc3]) - r = self.test_client.post("/coprs/{0}/{1}/update/" - .format(self.u1.name, self.c1.name), - data={"name": self.c1.name, - self.mc2.name: "y", - self.mc3.name: "y", - "id": self.c1.id}, - follow_redirects=True) - - assert "Project was updated successfully" in r.data - self.c1 = self.db.session.merge(self.c1) - self.mc1 = self.db.session.merge(self.mc1) - self.mc2 = self.db.session.merge(self.mc2) - self.mc3 = self.db.session.merge(self.mc3) - - mock_chroots = (self.models.MockChroot.query - .join(self.models.CoprChroot) - .filter(self.models.CoprChroot.copr_id == - self.c1.id).all()) - - mock_chroots_names = map(lambda x: x.name, mock_chroots) - assert self.mc2.name in mock_chroots_names - assert self.mc3.name in mock_chroots_names - assert self.mc1.name not in mock_chroots_names - - @TransactionDecorator("u2") - def test_update_deletes_multiple_chroots(self, f_users, f_coprs, - f_copr_permissions, - f_mock_chroots, f_db): - - # https://fedorahosted.org/copr/ticket/42 - self.db.session.add_all([self.u2, self.c2, self.mc1]) - # add one more mock_chroot, so that we can remove two - cc = self.models.CoprChroot() - cc.mock_chroot = self.mc1 - self.c2.copr_chroots.append(cc) - - r = self.test_client.post("/coprs/{0}/{1}/update/" - .format(self.u2.name, self.c2.name), - data={"name": self.c2.name, - self.mc1.name: "y", - "id": self.c2.id}, - follow_redirects=True) - - assert "Project was updated successfully" in r.data - self.c2 = self.db.session.merge(self.c2) - self.mc1 = self.db.session.merge(self.mc1) - mock_chroots = (self.models.MockChroot.query - .join(self.models.CoprChroot) - .filter(self.models.CoprChroot.copr_id == - self.c2.id).all()) - - assert len(mock_chroots) == 1 - - -class TestCoprApplyForPermissions(CoprsTestCase): - - @TransactionDecorator("u2") - def test_apply(self, f_users, f_coprs, f_db): - self.db.session.add_all([self.u1, self.u2, self.c1]) - r = self.test_client.post("/coprs/{0}/{1}/permissions_applier_change/" - .format(self.u1.name, self.c1.name), - data={"copr_builder": "1"}, - follow_redirects=True) - - assert "Successfuly updated" in r.data - - self.u1 = self.db.session.merge(self.u1) - self.u2 = self.db.session.merge(self.u2) - self.c1 = self.db.session.merge(self.c1) - new_perm = (self.models.CoprPermission.query - .filter(self.models.CoprPermission.user_id == self.u2.id) - .filter(self.models.CoprPermission.copr_id == self.c1.id) - .first()) - - assert new_perm.copr_builder == 1 - assert new_perm.copr_admin == 0 - - @TransactionDecorator("u1") - def test_apply_doesnt_lower_other_values_from_admin_to_request( - self, f_users, f_coprs, f_copr_permissions, f_db): - - self.db.session.add_all([self.u1, self.u2, self.cp1, self.c2]) - r = self.test_client.post("/coprs/{0}/{1}/permissions_applier_change/" - .format(self.u2.name, self.c2.name), - data={"copr_builder": 1, "copr_admin": "1"}, - follow_redirects=True) - assert "Successfuly updated" in r.data - - self.u1 = self.db.session.merge(self.u1) - self.c2 = self.db.session.merge(self.c2) - new_perm = (self.models.CoprPermission.query - .filter(self.models.CoprPermission.user_id == self.u1.id) - .filter(self.models.CoprPermission.copr_id == self.c2.id) - .first()) - - assert new_perm.copr_builder == 2 - assert new_perm.copr_admin == 1 - - -class TestCoprUpdatePermissions(CoprsTestCase): - - @TransactionDecorator("u2") - def test_cancel_permission(self, f_users, f_coprs, - f_copr_permissions, f_db): - - self.db.session.add_all([self.u2, self.c2]) - r = self.test_client.post("/coprs/{0}/{1}/update_permissions/" - .format(self.u2.name, self.c2.name), - data={"copr_builder_1": "0"}, - follow_redirects=True) - - # very volatile, but will fail fast if something changes - check_string = '' - assert check_string not in r.data - - @TransactionDecorator("u2") - def test_update_more_permissions(self, f_users, f_coprs, - f_copr_permissions, f_db): - - self.db.session.add_all([self.u2, self.c3]) - self.test_client.post("/coprs/{0}/{1}/update_permissions/" - .format(self.u2.name, self.c3.name), - data={"copr_builder_1": "2", - "copr_admin_1": "1", - "copr_admin_3": "2"}, - follow_redirects=True) - - self.u1 = self.db.session.merge(self.u1) - self.u3 = self.db.session.merge(self.u3) - self.c3 = self.db.session.merge(self.c3) - - u1_c3_perms = (self.models.CoprPermission.query - .filter(self.models.CoprPermission.copr_id == - self.c3.id) - .filter(self.models.CoprPermission.user_id == - self.u1.id) - .first()) - - assert (u1_c3_perms.copr_builder == - self.helpers.PermissionEnum("approved")) - assert (u1_c3_perms.copr_admin == - self.helpers.PermissionEnum("request")) - - u3_c3_perms = (self.models.CoprPermission.query - .filter(self.models.CoprPermission.copr_id == - self.c3.id) - .filter(self.models.CoprPermission.user_id == - self.u3.id) - .first()) - assert (u3_c3_perms.copr_builder == - self.helpers.PermissionEnum("nothing")) - assert (u3_c3_perms.copr_admin == - self.helpers.PermissionEnum("approved")) - - @TransactionDecorator("u1") - def test_copr_admin_can_update_permissions(self, f_users, f_coprs, - f_copr_permissions, f_db): - - self.db.session.add_all([self.u2, self.c3]) - r = self.test_client.post("/coprs/{0}/{1}/update_permissions/" - .format(self.u2.name, self.c3.name), - data={"copr_builder_1": "2", - "copr_admin_3": "2"}, - follow_redirects=True) - - assert "Project permissions were updated" in r.data - - @TransactionDecorator("u1") - def test_copr_admin_can_give_up_his_permissions(self, f_users, f_coprs, - f_copr_permissions, f_db): - # if admin is giving up his permission and there are more permissions for - # this copr, then if the admin is altered first, he won"t be permitted - # to alter the other permissions and the whole update would fail - self.db.session.add_all([self.u2, self.c3, self.cp2, self.cp3]) - # mock out the order of CoprPermission objects, so that we are sure - # the admin is the first one and therefore this fails if - # the view doesn"t reorder the permissions - flexmock(self.models.Copr, copr_permissions=[self.cp3, self.cp2]) - r = self.test_client.post("/coprs/{0}/{1}/update_permissions/" - .format(self.u2.name, self.c3.name), - data={"copr_admin_1": "1", - "copr_admin_3": "1"}, - follow_redirects=True) - - self.u1 = self.db.session.merge(self.u1) - self.c3 = self.db.session.merge(self.c3) - perm = (self.models.CoprPermission.query - .filter(self.models.CoprPermission.user_id == self.u1.id) - .filter(self.models.CoprPermission.copr_id == self.c3.id) - .first()) - - assert perm.copr_admin == 1 - assert "Project permissions were updated" in r.data - - -class TestCoprDelete(CoprsTestCase): - - @TransactionDecorator("u1") - def test_delete(self, f_users, f_coprs, f_db): - self.db.session.add_all([self.u1, self.c1]) - r = self.test_client.post("/coprs/{0}/{1}/delete/" - .format(self.u1.name, self.c1.name), - data={"verify": "yes"}, - follow_redirects=True) - - assert "Project was deleted successfully" in r.data - self.db.session.add(self.c1) - assert self.models.Action.query.first().id == self.c1.id - assert self.models.Copr.query.filter( - self.models.Copr.id == self.c1.id).first().deleted - - @TransactionDecorator("u1") - def test_copr_delete_does_not_delete_if_verify_filled_wrongly( - self, f_users, f_coprs, f_db): - - self.db.session.add_all([self.u1, self.c1]) - r = self.test_client.post("/coprs/{0}/{1}/delete/" - .format(self.u1.name, self.c1.name), - data={"verify": "no"}, - follow_redirects=True) - - assert "Project was deleted successfully" not in r.data - assert not self.models.Action.query.first() - assert self.models.Copr.query.filter( - self.models.Copr.id == self.c1.id).first() - - @TransactionDecorator("u2") - def test_non_owner_cant_delete(self, f_users, f_coprs, f_db): - self.db.session.add_all([self.u1, self.u2, self.c1]) - r = self.test_client.post("/coprs/{0}/{1}/delete/" - .format(self.u1.name, self.c1.name), - data={"verify": "yes"}, - follow_redirects=True) - self.c1 = self.db.session.merge(self.c1) - assert "Project was deleted successfully" not in r.data - assert not self.models.Action.query.first() - assert self.models.Copr.query.filter( - self.models.Copr.id == self.c1.id).first() - - -class TestCoprRepoGeneration(CoprsTestCase): - - @pytest.fixture - def f_custom_builds(self): - """ Custom builds are used in order not to break the default ones """ - self.b5 = self.models.Build( - copr=self.c1, user=self.u1, submitted_on=9, - ended_on=200, results="https://bar.baz") - self.b6 = self.models.Build( - copr=self.c1, user=self.u1, submitted_on=11) - self.b7 = self.models.Build( - copr=self.c1, user=self.u1, submitted_on=10, - ended_on=150, results="https://bar.baz") - self.mc1 = self.models.MockChroot( - os_release="fedora", os_version="18", arch="x86_64") - self.cc1 = self.models.CoprChroot(mock_chroot=self.mc1, copr=self.c1) - - # assign with chroots - for build in [self.b5, self.b6, self.b7]: - self.db.session.add( - self.models.BuildChroot( - build=build, - mock_chroot=self.mc1 - ) - ) - - self.db.session.add_all( - [self.b5, self.b6, self.b7, self.mc1, self.cc1]) - - @pytest.fixture - def f_not_finished_builds(self): - """ Custom builds are used in order not to break the default ones """ - self.b8 = self.models.Build( - copr=self.c1, user=self.u1, submitted_on=11) - self.mc1 = self.models.MockChroot( - os_release="fedora", os_version="18", arch="x86_64") - self.cc1 = self.models.CoprChroot(mock_chroot=self.mc1, copr=self.c1) - - # assign with chroot - self.db.session.add( - self.models.BuildChroot( - build=self.b8, - mock_chroot=self.mc1 - ) - ) - - self.db.session.add_all([self.b8, self.mc1, self.cc1]) - - def test_fail_on_missing_dash(self): - r = self.tc.get("/coprs/reponamewithoutdash/repo/") - assert r.status_code == 404 - assert "Copr with name repo does not exist" in r.data - - def test_fail_on_nonexistent_copr(self): - r = self.tc.get( - "/coprs/bogus-user/bogus-nonexistent-repo/repo/fedora-18-x86_64/") - assert r.status_code == 404 - assert "does not exist" in r.data - - def test_fail_on_no_finished_builds(self, f_users, f_coprs, - f_not_finished_builds, f_db): - - r = self.tc.get( - "/coprs/{0}/{1}/repo/fedora-18-x86_64/" - .format(self.u1.name, self.c1.name)) - - assert r.status_code == 404 - assert "Repository not initialized" in r.data - - def test_works_on_older_builds(self, f_users, f_coprs, - f_custom_builds, f_db): - r = self.tc.get( - "/coprs/{0}/{1}/repo/fedora-18-x86_64/" - .format(self.u1.name, self.c1.name)) - - assert r.status_code == 200 - assert "baseurl=https://bar.baz" in r.data diff --git a/frontend/LICENSE b/frontend/LICENSE new file mode 100644 index 0000000..d159169 --- /dev/null +++ b/frontend/LICENSE @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/frontend/copr-frontend.spec b/frontend/copr-frontend.spec new file mode 100644 index 0000000..d064a61 --- /dev/null +++ b/frontend/copr-frontend.spec @@ -0,0 +1,599 @@ +%global with_test 1 +%if 0%{?rhel} < 7 && 0%{?rhel} > 0 +%global _pkgdocdir %{_docdir}/%{name}-%{version} +%global __python2 %{__python} +%endif + +Name: copr-frontend +Version: 1.28 +Release: 1%{?dist} +Summary: Frontend for Copr + +Group: Applications/Productivity +License: GPLv2+ +URL: https://fedorahosted.org/copr/ +# Source is created by +# git clone https://git.fedorahosted.org/git/copr.git +# cd copr/frontend +# tito build --tgz +Source0: %{name}-%{version}.tar.gz + +BuildArch: noarch +BuildRequires: asciidoc +BuildRequires: libxslt +BuildRequires: util-linux +BuildRequires: python-setuptools +BuildRequires: python-requests +BuildRequires: python2-devel +BuildRequires: systemd +%if 0%{?rhel} < 7 && 0%{?rhel} > 0 +BuildRequires: python-argparse +%endif +#for doc package +BuildRequires: epydoc +BuildRequires: graphviz +BuildRequires: make + +Requires: httpd +Requires: mod_wsgi +Requires: python-alembic +Requires: python-flask +Requires: python-flask-openid +Requires: python-flask-wtf +Requires: python-flask-sqlalchemy +Requires: python-flask-script +Requires: python-flask-whooshee +#Requires: python-virtualenv +Requires: python-blinker +Requires: python-markdown +Requires: python-psycopg2 +Requires: python-pylibravatar +Requires: python-whoosh >= 2.5.3 +Requires: pytz +# for tests: +Requires: pytest +Requires: python-flexmock +Requires: python-decorator +Requires: yum +%if 0%{?rhel} < 7 && 0%{?rhel} > 0 +BuildRequires: python-argparse +%endif +# check +BuildRequires: python-flask +BuildRequires: python-flask-script +BuildRequires: python-flask-sqlalchemy +BuildRequires: python-flask-openid +BuildRequires: python-flask-whooshee +BuildRequires: python-pylibravatar +BuildRequires: python-flask-wtf +BuildRequires: pytest +BuildRequires: yum +BuildRequires: python-flexmock +BuildRequires: python-decorator +BuildRequires: python-markdown +BuildRequires: pytz + +%description +COPR is lightweight build system. It allows you to create new project in WebUI, +and submit new builds and COPR will create yum repository from latests builds. + +This package contains frontend. + +%package doc +Summary: Code documentation for COPR + +%description doc +COPR is lightweight build system. It allows you to create new project in WebUI, +and submit new builds and COPR will create yum repository from latests builds. + +This package include documentation for COPR code. Mostly useful for developers +only. + +%prep +%setup -q + + +%build +# build documentation +pushd documentation +make %{?_smp_mflags} python +popd + +%install + +install -d %{buildroot}%{_sysconfdir}/copr +install -d %{buildroot}%{_datadir}/copr/coprs_frontend +install -d %{buildroot}%{_sharedstatedir}/copr/data/openid_store +install -d %{buildroot}%{_sharedstatedir}/copr/data/openid_store/associations +install -d %{buildroot}%{_sharedstatedir}/copr/data/openid_store/nonces +install -d %{buildroot}%{_sharedstatedir}/copr/data/openid_store/temp +install -d %{buildroot}%{_sharedstatedir}/copr/data/whooshee +install -d %{buildroot}%{_sharedstatedir}/copr/data/whooshee/copr_user_whoosheer + +cp -a coprs_frontend/* %{buildroot}%{_datadir}/copr/coprs_frontend +mv %{buildroot}%{_datadir}/copr/coprs_frontend/coprs.conf.example ./ +mv %{buildroot}%{_datadir}/copr/coprs_frontend/config/* %{buildroot}%{_sysconfdir}/copr +rm %{buildroot}%{_datadir}/copr/coprs_frontend/CONTRIBUTION_GUIDELINES +touch %{buildroot}%{_sharedstatedir}/copr/data/copr.db + +cp -a documentation/python-doc %{buildroot}%{_pkgdocdir}/ + +%check +%if %{with_test} && %{_arch} == "x86_64" + pushd coprs_frontend + rm -rf /tmp/copr.db /tmp/whooshee || : + COPR_CONFIG="$(pwd)/config/copr_unit_test.conf" ./manage.py test + popd +%endif + +%pre +getent group copr-fe >/dev/null || groupadd -r copr-fe +getent passwd copr-fe >/dev/null || \ +useradd -r -g copr-fe -G copr-fe -d %{_datadir}/copr/coprs_frontend -s /bin/bash -c "COPR frontend user" copr-fe +/usr/bin/passwd -l copr-fe >/dev/null + +%post +service httpd condrestart + +%files +%doc LICENSE coprs.conf.example +%dir %{_datadir}/copr +%dir %{_sysconfdir}/copr +%dir %{_sharedstatedir}/copr +%{_datadir}/copr/coprs_frontend + +%defattr(-, copr-fe, copr-fe, -) +%dir %{_sharedstatedir}/copr/data +%dir %{_sharedstatedir}/copr/data/openid_store +%dir %{_sharedstatedir}/copr/data/whooshee +%dir %{_sharedstatedir}/copr/data/whooshee/copr_user_whoosheer + +%ghost %{_sharedstatedir}/copr/data/copr.db + +%defattr(600, copr-fe, copr-fe, 700) +%config(noreplace) %{_sysconfdir}/copr/copr.conf +%config(noreplace) %{_sysconfdir}/copr/copr_devel.conf +%config(noreplace) %{_sysconfdir}/copr/copr_unit_test.conf + +%files doc +%doc %{_pkgdocdir}/python-doc + +%changelog +* Thu Feb 27 2014 Miroslav Suchý 1.28-1 +- [backend] - pass lock to Actions + +* Wed Feb 26 2014 Miroslav Suchý 1.27-1 +- [frontend] update to jquery 1.11.0 +- [fronted] link username to fas +- [cli] allow to build into projects of other users +- [backend] do not create repo in destdir +- [backend] ensure that only one createrepo is running at the same time +- [cli] allow to get data from sent build +- temporary workaround for BZ 1065251 +- Chroot details API now uses GET instead of POST +- when deleting/canceling task, go to same page +- add copr modification to web api +- 1063311 - admin should be able to delete task +- [frontend] Stray end tag h4. +- [frontend] another s/coprs/projects/ rename +- [frontend] provide info about last successfull build +- [spec] rhel5 needs group definition even in subpackage +- [frontend] move 'you agree' text to dd +- [frontend] add margin to chroots-set +- [frontend] add margin to field label +- [frontend] put disclaimer to paragraph tags +- [frontend] use black font color +- [frontend] use default filter instead of *_not_filled +- [frontend] use markdown template filter +- [frontend] use isdigit instead of is_int +- [frontend] move Serializer to helpers +- [frontend] fix coding style and py3 compatibility +- [cli] fix coding style and py3 compatibility +- [backend] fix coding style and py3 compatibility + +* Tue Jan 28 2014 Miroslav Suchý 1.26-1 +- lower testing date +- move localized_time into filters +- [frontend] update user data after login +- [frontend] use iso-8601 date + +* Mon Jan 27 2014 Miroslav Suchý 1.25-1 +- 1044085 - move timezone modification out of template and make it actually + work +- clean up temp data if any +- [db] timezone can be nullable +- [frontend] actually save the timezone to model +- fix colision of revision id +- 1044085 - frontend: display time in user timezone +- [frontend] rebuild stuck task +- disable test on i386 +- use experimental createrepo_c to get rid of lock on temp files +- [frontend] - do not throw ISE when build_id is malformed +- [tests] add test for BuildLogic.add +- [tests] add test for build resubmission +- [frontend] permission checking is done in BuildLogic.add +- [frontend] remove BuildLogic.new, use BL.add only +- [api] fix validation error handling +- [cli] fix initial_pkgs and repos not sent to backend +- [frontend] fix BuildsLogic.new not assigning copr to build +- [frontend] allow resubmitting builds from monitor +- [frontend] allow GET on repeat_build +- [frontend] 1050904 - monitor shows not submitted chroots +- [frontend] rename active_mock_chroots to active_chroots +- [frontend] rename MockChroot.chroot_name to .name +- [frontend] 1054474 - drop Copr.build_count nonsense +- [tests] fix https and repo generation +- [tests] return exit code from manage.py test +- 1054472 - Fix deleting multiple SRPMs +- [spec] tighten acl on copr-be.conf +- [backend] - add missing import +- 1054082 - general: encode to utf8 if err in mimetext +- [backend] lock log file before writing +- 1055594 - mockremote: always unquote pkg url +- 1054086 - change vendor tag +- mockremote: rawhide instead of $releasever in repos when in rawhide chroot +- 1055499 - do not replace version with $releasever on rawhide +- 1055119 - do not propagate https until it is properly signed +- fix spellings on chroot edit page +- 1054341 - be more verbose about allowed licenses +- 1054594 - temporary disable https in repo file + +* Thu Jan 16 2014 Miroslav Suchý 1.24-1 +- add BR python-markdown +- [fronted] don't add description to .repo files +- [spec] fix with_tests conditional +- add build deletion +- 1044158 - do not require fas username prior to login +- replace http with https in copr-cli and in generated repo file +- [cli] UX changes - explicitely state that pkgs is URL +- 1053142 - only build copr-cli on el6 +- [frontend] correctly handle mangled chroot +- [frontend] do not traceback when user malform url +- [frontend] change default description and instructions to sound more + dangerously +- 1052075 - do not set chroots on repeated build +- 1052071 - do not throw ISE when copr does not exist + +* Mon Jan 13 2014 Miroslav Suchý 1.23-1 +- [backend] rhel7-beta do not have comps +- 1052073 - correctly parse malformed chroot + +* Fri Jan 10 2014 Miroslav Suchý 1.22-1 +- [backend] if we could not spawn VM, wait a moment and try again +- [backend] use createrepo_c instead of createrepo +- 1050952 - check if copr_url exist in config +- [frontend] replace newlines in description by space in repo file + +* Wed Jan 08 2014 Miroslav Suchý 1.21-1 +- 1049460 - correct error message +- [cron] manualy clean /var/tmp after createrepo + +* Wed Jan 08 2014 Miroslav Suchý 1.20-1 +- [cli] no need to set const with action=store_true +- [cli] code cleanup +- 1049460 - print nice error when projects does not exist +- 1049392 - require python-setuptools +- [backend] add --verbose to log to stderr +- [backend] handle KeyboardInterrupt without tons of tracebacks +- 1048508 - fix links at projects lists +- [backend] in case of error the output is in e.output +- [selinux] allow httpd to search +- [backend] set number of worker in name of process +- [logrotate] rotate every week unconditionally +- [backend] do not traceback if jobfile is mangled +- [backend] print error messages to stderr +- [cli] do not require additional arguments for --nowait +- [backend] replace procname with setproctitle +- [cli] use copr.fedoraproject.org as default url +- [frontend] show monitor even if last build have been canceled +- [backend] call correct function +- [cli] print errors to stderr +- 1044136 - do not print TB if config in mangled +- 1044165 - Provide login and token information in the same form as entered to + ~/.config-copr +- [frontend] code cleanup +- [frontend] move rendering of .repo file to helpers +- 1043649 - in case of Fedora use $releasever in repo file +- [frontend] condition should be in reverse + +* Mon Dec 16 2013 Miroslav Suchý 1.19-1 +- [backend] log real cause if ansible crash +- [frontend] try again if whoosh does not get lock +- [backend] if frontend does not respond, repeat +- print yum repos nicely +- Bump the copr-cli release to 0.2.0 with all the changes made +- Refer to the man page for more information about the configuration file for + copr-cli +- Rework the layout of the list command +- Fix parsing the copr_url from the configuration file +- [backend] run createrepo as copr user +- 1040615 - wrap lines with long URL + +* Wed Dec 11 2013 Miroslav Suchý 1.18-1 +- [frontend] inicialize variable + +* Wed Dec 11 2013 Miroslav Suchý 1.17-1 +- [frontend] fix latest build variable overwrite + +* Wed Dec 11 2013 Miroslav Suchý 1.16-1 +- [backend] store jobs in id-chroot.json file +- [frontend] handle unknown build/chroot status +- use newstyle ansible variables + +* Tue Dec 10 2013 Miroslav Suchý 1.15-1 +- [frontend] smarter package name parsing +- [frontend] extend range to allow 0 +- handle default timeout on backend +- initial support for SCL +- [backend] create word readable files in result directory +- [backend] print tracebacks +- [frontend] monitor: display only pkg name w/o version +- [doc] update api docs +- [doc] update copr-cli manpage +- [cli] list only name, description and instructions +- [cli] add support for build status & build monitor +- [frontend] add build status to API +- [playbook] do not overwrite mockchain +- [backend] add spece between options +- [backend] pass mock options correctly +- [frontend] support markdown in description and instructions +- [backend] Add macros to mockchain define arguments +- [backend] Pass copr username and project name to MockRemote +- [backend] Handle additional macro specification in MockRemote +- [frontend] monitor: show results per package +- [frontend] add favicon +- [backend] quote strings before passing to mockchain +- send chroots with via callback to frontend +- [cli] change cli to new api call +- enhance API documentation +- add yum_repos to coprs/user API call +- [frontend] provide link to description of allowed content +- [backend] we pass just one chroot +- [backend] - variable play is not defined +- if createrepo fail, run it again +- [cron] fix syntax error +- [man] state that --chroot for create command is required +- [spec] enable tests +- [howto] add note about upgrading db schema +- [frontend]: add copr monitor +- [tests]: replace test_allowed_one +- [tests]: fix for BuildChroots & new backend view +- [frontend] rewrite backend view to use Build <-> Chroot relation +- [frontend] add Build <-> Chroot relation +- 1030493 - [cli] check that at least one chroot is entered +- [frontend] typo +- fixup! [tests]: fix test_build_logic to handle BuildChroot +- fixup! [frontend] add ActionsLogic +- [tests]: fix test_build_logic to handle BuildChroot +- [spec] enable/disable test using variable +- add migration script - add table build_chroot +- [frontend] skip legal-flag actions when dumping waiting actions +- [frontend] rewrite backend view to use Build <-> Chroot relation +- [frontend] add ActionsLogic +- [frontend] create BuildChroot objects on new build +- [frontend] add Build <-> Chroot relation +- [frontend] add StatusEnum +- [frontend] fix name -> coprname typo +- [frontend] remove unused imports +- [frontend] add missing json import +- [backend] rework ip address extraction +- ownership of /etc/copr should be just normal +- [backend] - wrap up returning action in "action" blok +- [backend] rename backend api url +- [backend] handle "rename" action +- [backend] handle "delete" action +- base handling of actions +- move callback to frontend to separate object +- secure waiting_actions with password +- pick only individual builds +- make address, where we send legal flags, configurable +- send email to root after legal flag have been raised + +* Fri Nov 08 2013 Miroslav Suchý 1.14-1 +- 1028235 - add disclaimer about repos +- fix pagination +- fix one failing test + +* Wed Nov 06 2013 Miroslav Suchý 1.13-1 +- suggest correct name of repo file +- we could not use releasever macro +- no need to capitalize Projects +- another s/copr/project +- add link to header for sign-in +- fix failing tests +- UX - let textarea will full widht of box +- UX - make background of hovered builds darker +- generate yum repo for each chroot of copr +- align table header same way as ordinary rows +- enable resulting repo and disable gpgchecks + +* Mon Nov 04 2013 Miroslav Suchý 1.12-1 +- do not send parameters when we neither need them nor use them +- authenticate using api login, not using username +- disable editing name of project +- Add commented out WTF_CSRF_ENABLED = True to configs +- Use new session for each test +- fix test_coprs_general failures +- fix test_coprs_builds failures +- Add WTF_CSRF_ENABLED = False to unit test config +- PEP8 fixes +- Fix compatibility with wtforms 0.9 +- typo s/submited/submitted/ +- UX - show details of build only after click +- add link to FAQ to footer +- UX - add placeholders +- UX - add asterisk to required fields +- dynamicly generate url for home +- add footer + +* Sat Oct 26 2013 Miroslav Suchý 1.11-1 +- catch IOError from libravatar if there is no network + +* Fri Oct 25 2013 Miroslav Suchý 1.10-1 +- do not normalize url +- specify full prefix of http +- execute playbook using /usr/bin/ansible-playbook +- use ssh transport +- check after connection is made +- add notes about debuging mockremote +- clean up instance even when worker fails +- normalize paths before using +- do not use exception variable +- operator should be preceded and followed by space +- remove trailing whitespace +- convert comment to docstring +- use ssh transport +- do not create new ansible connection, reuse self.conn +- run copr-be.py as copr +- s/Copr/Project/ where we use copr in meaning of projects +- number will link to those coprs, to which it refers +- run log and jobgrab as copr user +- log event to log file +- convert comment into docstring +- use unbufferred output for copr-be.py +- hint how to set ec2 variables +- document sleeptime +- document copr_url for copr-cli +- document how to set api key for copr-cli +- do not create list of list +- document SECRET_KEY variable +- make note how to become admin +- instruct people to install selinux with frontend + +* Thu Oct 03 2013 Miroslav Suchý 1.9-1 +- prune old builds +- require python-decorator +- remove requirements.txt +- move TODO-backend to our wiki +- create pid file in /var/run/copr-backend +- add backend service file for systemd +- remove daemonize option in config +- use python logging +- create pid file in /var/run by default +- do not create destdir +- use daemon module instead of home brew function +- fix default location of copr-be.conf +- 2 tests fixed, one still failing +- fix failing test test_fail_on_missing_dash +- fixing test_fail_on_nonexistent_copr test +- run frontend unit tests when building package +- Adjust URLs in the unit-tests to their new structure +- Adjust the CLI to call the adjuste endpoint of the API +- Adjust API endpoint to reflects the UI endpoints in their url structure +- First pass at adding fedmsg hooks. + +* Tue Sep 24 2013 Miroslav Suchý 1.8-1 +- 1008532 - require python2-devel +- add note about ssh keys to copr-setup.txt +- set home of copr user to system default + +* Mon Sep 23 2013 Miroslav Suchý 1.7-1 +- 1008532 - backend should own _pkgdocdir +- 1008532 - backend should owns /etc/copr as well +- 1008532 - require logrotate +- 1008532 - do not distribute empty copr.if +- 1008532 - use %%{?_smp_mflags} macro with make +- move jobsdir to /var/lib/copr/jobs +- correct playbooks path +- selinux with enforce can be used for frontend + +* Wed Sep 18 2013 Miroslav Suchý 1.6-1 +- add BR python-devel +- generate selinux type for /var/lib/copr and /var/log/copr +- clean up backend setup instructions +- initial selinux subpackage + +* Mon Sep 16 2013 Miroslav Suchý 1.5-1 +- 1008532 - use __python2 instead of __python +- 1008532 - do not mark man page as doc +- 1008532 - preserve timestamp + +* Mon Sep 16 2013 Miroslav Suchý 1.4-1 +- add logrotate file + +* Mon Sep 16 2013 Miroslav Suchý 1.3-1 +- be clear how we create tgz + +* Mon Sep 16 2013 Miroslav Suchý 1.2-1 +- fix typo +- move frontend data into /var/lib/copr +- no need to own /usr/share/copr by copr-fe +- mark application as executable +- coprs_frontend does not need to be owned by copr-fe +- add executable attribute to copr-be.py +- remove shebang from dispatcher.py +- squeeze description into 80 chars +- fix typo +- frontend need argparse too +- move results into /var/lib/copr/public_html +- name of dir is just copr-%%version +- Remove un-necessary quote that breaks the tests +- Adjust unit-tests to the new urls +- Update the URL to be based upon a /user/copr/ structure +- comment config copr-be.conf and add defaults +- put examples of builderpb.yml and terminatepb.yml into doc dir +- more detailed description of copr-be.conf +- move files in config directory not directory itself +- include copr-be.conf +- include copr-be.py +- create copr with lighttpd group +- edit backend part of copr-setup.txt +- remove fedora16 and add 19 and 20 +- create -doc subpackage with python documentation +- add generated documentation on gitignore list +- add script to generate python documentation +- copr-setup.txt change to for mock +- rhel6 do not know _pkgdocdir macro +- make instruction clear +- require recent whoosh +- add support for libravatar +- include backend in rpm +- add notes about lighttpd config files and how to deploy them +- do not list file twice +- move log file to /var/log +- change destdir in copr-be.conf.example +- lightweight is the word and buildsystem has more meaning than 'koji'. +- restart apache after upgrade of frontend +- own directory where backend put results +- removal of hidden-file-or-dir + /usr/share/copr/coprs_frontend/coprs/logic/.coprs_logic.py.swo +- copr-backend.noarch: W: spelling-error %%description -l en_US latests -> + latest, latest's, la tests +- simplify configuration - introduce /etc/copr/copr*.conf +- Replace "with" statements with @TransactionDecorator decorator +- add python-flexmock to deps of frontend +- remove sentence which does not have meaning +- change api token expiration to 120 days and make it configurable +- create_chroot must be run as copr-fe user +- add note that you have to add chroots to db +- mark config.py as config so it is not overwritten during upgrade +- own directory data/whooshee/copr_user_whoosheer +- gcc is not needed +- sqlite db must be owned by copr-fe user +- copr does not work with selinux +- create subdirs under data/openid_store +- suggest to install frontend as package from copr repository +- on el6 add python-argparse to BR +- add python-requests to BR +- add python-setuptools to BR +- maintain apache configuration on one place only +- apache 2.4 changed access control +- require python-psycopg2 +- postgresql server is not needed +- document how to create db +- add to HOWTO how to create db +- require python-alembic +- add python-flask-script and python-flask-whooshee to requirements +- change user in coprs.conf.example to copr-fe +- fix paths in coprs.conf.example +- copr is noarch package +- add note where to configure frontend +- move frontend to /usr/share/copr/coprs_frontend +- put production placeholders in coprs_frontend/coprs/config.py +- put frontend into copr.spec +- web application should be put in /usr/share/%%{name} + +* Mon Jun 17 2013 Miroslav Suchý 1.1-1 +- new package built with tito + + diff --git a/frontend/coprs_frontend/CONTRIBUTION_GUIDELINES b/frontend/coprs_frontend/CONTRIBUTION_GUIDELINES new file mode 100644 index 0000000..0513e4e --- /dev/null +++ b/frontend/coprs_frontend/CONTRIBUTION_GUIDELINES @@ -0,0 +1,20 @@ +This file contains some "should" rules, that are good to follow. + +- coprs.logic +-- The *Logic objects should be named after the primary object that they + work with, but pluralized. E.g. CoprChroot->CoprChrootsLogic +-- The methods of *Logic objects should generally be @classmethod. +-- The methods of *Logic objects should accept "user" as a second argument + (after the "cls" argument). This argument should contain object of user + who is performing the action. +-- The methods of *Logic objects shouldn't call db.session.commit(). This + should be called in views that use the methods. +-- The usual names of methods are (each of the methods can perform certain + checks, e.g. authorization, correct parameters, ...): +--- "add" for creating objects and adding them to session +--- "new" for just adding objects to session +--- "get" for getting a query object for a single model object +--- "get_multiple" for getting a query object for multiple model objects +--- "edit" for editing objects and adding them to session +--- "update" for just adding altered objects to session +--- "delete" for deleting an object diff --git a/frontend/coprs_frontend/alembic.ini b/frontend/coprs_frontend/alembic.ini new file mode 100644 index 0000000..7c7be19 --- /dev/null +++ b/frontend/coprs_frontend/alembic.ini @@ -0,0 +1,50 @@ +# A generic, single database configuration. + +[alembic] +# path to migration scripts +script_location = alembic + +# template used to generate migration files +# file_template = %%(rev)s_%%(slug)s + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + +# sqlalchemy.url = driver://user:pass@localhost/dbname + + +# Logging configuration +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/frontend/coprs_frontend/alembic/env.py b/frontend/coprs_frontend/alembic/env.py new file mode 100644 index 0000000..530f4e6 --- /dev/null +++ b/frontend/coprs_frontend/alembic/env.py @@ -0,0 +1,73 @@ +from __future__ import with_statement +from alembic import context +from logging.config import fileConfig + +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config + +# Interpret the config file for Python logging. +# This line sets up loggers basically. +fileConfig(config.config_file_name) + +# add your model's MetaData object here +# for 'autogenerate' support +# from myapp import mymodel +# target_metadata = mymodel.Base.metadata +import sys +import os + +# alembic doesn't include cwd by default +sys.path.append(os.getcwd()) + +from coprs import db +target_metadata = db.metadata + +# other values from the config, defined by the needs of env.py, +# can be acquired: +# my_important_option = config.get_main_option("my_important_option") +# ... etc. + + +def run_migrations_offline(): + """Run migrations in 'offline' mode. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + + """ + url = config.get_main_option("sqlalchemy.url") + context.configure(url=url) + + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online(): + """Run migrations in 'online' mode. + + In this scenario we need to create an Engine + and associate a connection with the context. + + """ + connection = db.engine.connect() + context.configure( + connection=connection, + target_metadata=target_metadata + ) + + try: + with context.begin_transaction(): + context.run_migrations() + finally: + connection.close() + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/frontend/coprs_frontend/alembic/script.py.mako b/frontend/coprs_frontend/alembic/script.py.mako new file mode 100644 index 0000000..9570201 --- /dev/null +++ b/frontend/coprs_frontend/alembic/script.py.mako @@ -0,0 +1,22 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision} +Create Date: ${create_date} + +""" + +# revision identifiers, used by Alembic. +revision = ${repr(up_revision)} +down_revision = ${repr(down_revision)} + +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +def upgrade(): + ${upgrades if upgrades else "pass"} + + +def downgrade(): + ${downgrades if downgrades else "pass"} diff --git a/frontend/coprs_frontend/alembic/versions/1ee4b45f5476_remove_fulltext_in_favor_of_whoosh.py b/frontend/coprs_frontend/alembic/versions/1ee4b45f5476_remove_fulltext_in_favor_of_whoosh.py new file mode 100644 index 0000000..21c0efc --- /dev/null +++ b/frontend/coprs_frontend/alembic/versions/1ee4b45f5476_remove_fulltext_in_favor_of_whoosh.py @@ -0,0 +1,31 @@ +"""empty message + +Revision ID: 1ee4b45f5476 +Revises: 3a035889852c +Create Date: 2013-02-14 14:11:50.624673 + +""" + +# revision identifiers, used by Alembic. +revision = "1ee4b45f5476" +down_revision = "3a035889852c" + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + ### commands auto generated by Alembic - please adjust! ### + op.drop_column("copr", u"copr_ts_col") + if op.get_bind().dialect.name == "postgresql": + op.execute("DROP trigger IF EXISTS copr_ts_update ON copr") + elif op.get_bind().dialect.name == "sqlite": + op.execute("DROP trigger IF EXISTS copr_ts_update") + op.execute("DROP trigger IF EXISTS copr_ts_insert") + ### end Alembic commands ### + + +def downgrade(): + ### commands auto generated by Alembic - please adjust! ### + op.add_column("copr", sa.Column(u"copr_ts_col", sa.TEXT(), nullable=True)) + ### end Alembic commands ### diff --git a/frontend/coprs_frontend/alembic/versions/246fd2dbf398_add_legal_flag.py b/frontend/coprs_frontend/alembic/versions/246fd2dbf398_add_legal_flag.py new file mode 100644 index 0000000..d8f4322 --- /dev/null +++ b/frontend/coprs_frontend/alembic/versions/246fd2dbf398_add_legal_flag.py @@ -0,0 +1,36 @@ +"""add_legal_flag + +Revision ID: 246fd2dbf398 +Revises: d062c3d9c00 +Create Date: 2013-04-03 10:39:54.837803 + +""" + +# revision identifiers, used by Alembic. +revision = "246fd2dbf398" +down_revision = "d062c3d9c00" + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + ### commands auto generated by Alembic - please adjust! ### + op.add_column( + "legal_flag", sa.Column("resolved_on", sa.Integer(), nullable=True)) + op.add_column( + "legal_flag", sa.Column("raised_on", sa.Integer(), nullable=True)) + op.drop_column("legal_flag", u"state") + op.drop_column("legal_flag", u"resolve_message") + ### end Alembic commands ### + + +def downgrade(): + ### commands auto generated by Alembic - please adjust! ### + op.add_column("legal_flag", sa.Column( + u"resolve_message", sa.TEXT(), nullable=True)) + op.add_column( + "legal_flag", sa.Column(u"state", sa.INTEGER(), nullable=True)) + op.drop_column("legal_flag", "raised_on") + op.drop_column("legal_flag", "resolved_on") + ### end Alembic commands ### diff --git a/frontend/coprs_frontend/alembic/versions/294405dfc7c0_add_action_data_fiel.py b/frontend/coprs_frontend/alembic/versions/294405dfc7c0_add_action_data_fiel.py new file mode 100644 index 0000000..afe396b --- /dev/null +++ b/frontend/coprs_frontend/alembic/versions/294405dfc7c0_add_action_data_fiel.py @@ -0,0 +1,24 @@ +"""add Action.data field + +Revision ID: 294405dfc7c0 +Revises: 3a415c6392bc +Create Date: 2014-01-20 15:43:09.986912 + +""" + +# revision identifiers, used by Alembic. +revision = "294405dfc7c0" +down_revision = "3a415c6392bc" + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + """ Add "data" colum to action table. """ + op.add_column("action", sa.Column("data", sa.Text())) + + +def downgrade(): + """ Drop "data" colum from action table. """ + op.drop_column("action", "data") diff --git a/frontend/coprs_frontend/alembic/versions/2a75f0a06d90_add_a_api_login_fiel.py b/frontend/coprs_frontend/alembic/versions/2a75f0a06d90_add_a_api_login_fiel.py new file mode 100644 index 0000000..980b707 --- /dev/null +++ b/frontend/coprs_frontend/alembic/versions/2a75f0a06d90_add_a_api_login_fiel.py @@ -0,0 +1,26 @@ +"""Add a api_login field to user + +Revision ID: 2a75f0a06d90 +Revises: 544873aa3ba1 +Create Date: 2013-03-10 10:01:16.820499 + +""" + +# revision identifiers, used by Alembic. +revision = "2a75f0a06d90" +down_revision = "544873aa3ba1" + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + """ Add the colum "api_login" to the table user. """ + op.add_column("user", sa.Column("api_login", sa.String(40), + nullable=False, + server_default="default_token")) + + +def downgrade(): + """ Drop the column "api_login" from the table user. """ + op.drop_column("user", "api_login") diff --git a/frontend/coprs_frontend/alembic/versions/2e30169e58ce_change_api_token_len.py b/frontend/coprs_frontend/alembic/versions/2e30169e58ce_change_api_token_len.py new file mode 100644 index 0000000..2e8c9bf --- /dev/null +++ b/frontend/coprs_frontend/alembic/versions/2e30169e58ce_change_api_token_len.py @@ -0,0 +1,30 @@ +"""Change api_token length from varchar(40) to varchar(255) + +Revision ID: 2e30169e58ce +Revises: 32ba137a3d56 +Create Date: 2013-01-08 19:42:16.562926 + +""" + +# revision identifiers, used by Alembic. +revision = '2e30169e58ce' +down_revision = '32ba137a3d56' + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + """ Change the api_token field from the user table from varchar(40) to + varchar(255). + """ + if op.get_bind().dialect.name != 'sqlite': + op.alter_column("user", "api_token", type_=sa.String(255)) + + +def downgrade(): + """ Change the api_token field from the user table from varchar(255) to + varchar(40). + """ + if op.get_bind().dialect.name != 'sqlite': + op.alter_column("user", "api_token", type_=sa.String(40)) diff --git a/frontend/coprs_frontend/alembic/versions/2fa80e062525_add_mock_chroots.py b/frontend/coprs_frontend/alembic/versions/2fa80e062525_add_mock_chroots.py new file mode 100644 index 0000000..9b2e402 --- /dev/null +++ b/frontend/coprs_frontend/alembic/versions/2fa80e062525_add_mock_chroots.py @@ -0,0 +1,111 @@ +"""empty message + +Revision ID: 2fa80e062525 +Revises: 2e30169e58ce +Create Date: 2013-01-14 09:04:42.768432 + +""" + +# revision identifiers, used by Alembic. +revision = "2fa80e062525" +down_revision = "2e30169e58ce" + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + ### commands auto generated by Alembic - please adjust! ### + op.create_table("mock_chroot", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column( + "os_release", sa.String(length=50), nullable=False), + sa.Column( + "os_version", sa.String(length=50), nullable=False), + sa.Column("arch", sa.String(length=50), nullable=False), + sa.Column("is_active", sa.Boolean(), nullable=False), + sa.PrimaryKeyConstraint("id") + ) + op.create_table("copr_chroot", + sa.Column("mock_chroot_id", sa.Integer(), nullable=False), + sa.Column("copr_id", sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(["copr_id"], ["copr.id"], ), + sa.ForeignKeyConstraint( + ["mock_chroot_id"], ["mock_chroot.id"], ), + sa.PrimaryKeyConstraint("mock_chroot_id", "copr_id") + ) + + # transfer the data - we can"t assume how the code looks like when + # running the migration, so do everything from scratch + metadata = sa.MetaData() + # just what we need of copr table + coprs_table = sa.Table("copr", metadata, sa.Column( + "chroots", sa.Text()), sa.Column("id", sa.Integer())) + # get chroots + chroots = set() + for cs in op.get_bind().execute(sa.select([coprs_table.c.chroots])): + chroots.update(set(cs[0].split(" "))) + chroots = list(chroots) + + mc_table = sa.Table("mock_chroot", metadata, + sa.Column("id", sa.Integer(), nullable=False), + sa.Column( + "os_release", sa.String(length=50), nullable=False), + sa.Column( + "os_version", sa.String(length=50), nullable=False), + sa.Column( + "arch", sa.String(length=50), nullable=False), + sa.Column("is_active", sa.Boolean(), nullable=False), + ) + cc_table = sa.Table("copr_chroot", metadata, + sa.Column( + "mock_chroot_id", sa.Integer(), nullable=False), + sa.Column("copr_id", sa.Integer(), nullable=False), + ) + # each mock_chroot now has id of value i + 1 (not to include 0) + for i, c in enumerate(chroots): + sc = c.split("-") + op.bulk_insert(mc_table, [ + {"id": i + 1, + "os_release": sc[0], + "os_version": sc[1], + "arch": sc[2], + "is_active": True}]) + + # insert proper copr_chroots for every copr + for row in op.get_bind().execute(sa.select([coprs_table.c.id, coprs_table.c.chroots])): + for c in row[1].split(" "): + op.bulk_insert( + cc_table, [{"mock_chroot_id": chroots.index(c) + 1, + "copr_id": row[0]}]) + + if op.get_bind().dialect.name == "sqlite": + op.rename_table("copr", "copr_1") + op.create_table("copr", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column( + "name", sa.String(length=100), nullable=False), + sa.Column("repos", sa.Text(), nullable=True), + sa.Column("created_on", sa.Integer(), nullable=True), + sa.Column("build_count", sa.Integer(), nullable=True), + sa.Column("owner_id", sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(["owner_id"], ["user.id"], ), + sa.PrimaryKeyConstraint("id") + ) + op.execute( + "INSERT INTO copr SELECT id,name,repos,created_on,build_count,owner_id FROM copr_1") + op.drop_table("copr_1") + else: + op.drop_column("copr", u"chroots") + ### end Alembic commands ### + + +def downgrade(): + ### commands auto generated by Alembic - please adjust! ### + op.add_column("copr", sa.Column( + u"chroots", sa.TEXT(), nullable=False, + server_default="fedora-rawhide-x86_64")) + + op.drop_table("copr_chroot") + op.drop_table("mock_chroot") + ### end Alembic commands ### diff --git a/frontend/coprs_frontend/alembic/versions/32ba137a3d56_add_token_informatio.py b/frontend/coprs_frontend/alembic/versions/32ba137a3d56_add_token_informatio.py new file mode 100644 index 0000000..f9f9161 --- /dev/null +++ b/frontend/coprs_frontend/alembic/versions/32ba137a3d56_add_token_informatio.py @@ -0,0 +1,33 @@ +"""Add token information to the user table + +Revision ID: 32ba137a3d56 +Revises: 595a31c145fb +Create Date: 2013-01-07 20:56:14.698735 + +""" + +# revision identifiers, used by Alembic. +revision = "32ba137a3d56" +down_revision = "595a31c145fb" + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + """ Add the coluns api_token and api_token_expiration to the user table. + """ + op.add_column("user", sa.Column("api_token", sa.String(40), + nullable=False, + server_default="default_token")) + + op.add_column("user", sa.Column("api_token_expiration", sa.Date, + nullable=False, + server_default="2000-1-1")) + + +def downgrade(): + """ Drop the coluns api_token and api_token_expiration to the user table. + """ + op.drop_column("user", "api_token") + op.drop_column("user", "api_token_expiration") diff --git a/frontend/coprs_frontend/alembic/versions/3a035889852c_add_copr_fulltext.py b/frontend/coprs_frontend/alembic/versions/3a035889852c_add_copr_fulltext.py new file mode 100644 index 0000000..36a94c8 --- /dev/null +++ b/frontend/coprs_frontend/alembic/versions/3a035889852c_add_copr_fulltext.py @@ -0,0 +1,79 @@ +"""add_copr_fulltext + +Revision ID: 3a035889852c +Revises: 3c3cce7a5fe0 +Create Date: 2013-02-01 10:06:37.034495 + +""" + +# revision identifiers, used by Alembic. +revision = '3a035889852c' +down_revision = '3c3cce7a5fe0' + +from alembic import op +import sqlalchemy as sa +from sqlalchemy import types +from sqlalchemy.ext import compiler + + +class Tsvector(types.UnicodeText): + pass + + +@compiler.compiles(Tsvector, 'postgresql') +def compile_tsvector(element, compiler, **kw): + return 'tsvector' + + +@compiler.compiles(Tsvector, 'sqlite') +def compile_tsvector(element, compiler, **kw): + return 'text' + + +def upgrade(): + ### commands auto generated by Alembic - please adjust! ### + op.add_column('copr', sa.Column('copr_ts_col', Tsvector(), nullable=True)) + op.create_index( + 'copr_ts_idx', 'copr', ['copr_ts_col'], postgresql_using='gin') + + session = sa.orm.sessionmaker(bind=op.get_bind())() + metadata = sa.MetaData() + if op.get_bind().dialect.name == 'postgresql': + op.execute("UPDATE copr \ + SET copr_ts_col = to_tsvector('pg_catalog.english', coalesce(name, '') || ' ' || \ + coalesce(description, '') || ' ' || coalesce(instructions, ''))") + # no need to coalesce here, the trigger doesn't need it + op.execute("CREATE TRIGGER copr_ts_update BEFORE INSERT OR UPDATE \ + ON copr \ + FOR EACH ROW EXECUTE PROCEDURE \ + tsvector_update_trigger(copr_ts_col, 'pg_catalog.english', name, description, instructions);") + elif op.get_bind().dialect.name == 'sqlite': + op.execute("UPDATE copr \ + SET copr_ts_col = coalesce(name, '') || ' ' || \ + coalesce(description, '') || ' ' || coalesce(instructions, '')") + # two triggers for sqlite... + op.execute("CREATE TRIGGER copr_ts_update \ + AFTER UPDATE OF name, description, instructions \ + ON copr \ + FOR EACH ROW \ + BEGIN \ + UPDATE copr SET copr_ts_col = coalesce(name, '') || ' ' || \ + coalesce(description, '') || ' ' || coalesce(instructions, ''); \ + END;") + op.execute("CREATE TRIGGER copr_ts_insert \ + AFTER INSERT \ + ON copr \ + FOR EACH ROW \ + BEGIN \ + UPDATE copr SET copr_ts_col = coalesce(name, '') || ' ' || \ + coalesce(description, '') || ' ' || coalesce(instructions, ''); \ + END;") + ### end Alembic commands ### + + +def downgrade(): + ### commands auto generated by Alembic - please adjust! ### + op.drop_column('copr', 'copr_ts_col') + if op.get_bind().dialect.name == 'postgresql': + op.execute("DROP TRIGGER copr_ts_update ON copr") + ### end Alembic commands ### diff --git a/frontend/coprs_frontend/alembic/versions/3a415c6392bc_add_buildroot_pkgs_c.py b/frontend/coprs_frontend/alembic/versions/3a415c6392bc_add_buildroot_pkgs_c.py new file mode 100644 index 0000000..fc98197 --- /dev/null +++ b/frontend/coprs_frontend/alembic/versions/3a415c6392bc_add_buildroot_pkgs_c.py @@ -0,0 +1,23 @@ +"""add buildroot_pkgs column + +Revision ID: 3a415c6392bc +Revises: 52e53e7b413e +Create Date: 2013-11-28 15:46:24.860025 + +""" + +# revision identifiers, used by Alembic. +revision = "3a415c6392bc" +down_revision = "52e53e7b413e" + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + op.add_column("copr_chroot", sa.Column( + "buildroot_pkgs", sa.Text(), nullable=True)) + + +def downgrade(): + op.drop_column("copr_chroot", "buildroot_pkgs") diff --git a/frontend/coprs_frontend/alembic/versions/3c3cce7a5fe0_add_copr_desc_and_instruct.py b/frontend/coprs_frontend/alembic/versions/3c3cce7a5fe0_add_copr_desc_and_instruct.py new file mode 100644 index 0000000..77a612d --- /dev/null +++ b/frontend/coprs_frontend/alembic/versions/3c3cce7a5fe0_add_copr_desc_and_instruct.py @@ -0,0 +1,28 @@ +"""empty message + +Revision ID: 3c3cce7a5fe0 +Revises: 2fa80e062525 +Create Date: 2013-01-22 09:42:39.037642 + +""" + +# revision identifiers, used by Alembic. +revision = "3c3cce7a5fe0" +down_revision = "2fa80e062525" + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + ### commands auto generated by Alembic - please adjust! ### + op.add_column("copr", sa.Column("description", sa.Text(), nullable=True)) + op.add_column("copr", sa.Column("instructions", sa.Text(), nullable=True)) + ### end Alembic commands ### + + +def downgrade(): + ### commands auto generated by Alembic - please adjust! ### + op.drop_column("copr", "instructions") + op.drop_column("copr", "description") + ### end Alembic commands ### diff --git a/frontend/coprs_frontend/alembic/versions/451e9507b866_generalize_action.py b/frontend/coprs_frontend/alembic/versions/451e9507b866_generalize_action.py new file mode 100644 index 0000000..3622cf4 --- /dev/null +++ b/frontend/coprs_frontend/alembic/versions/451e9507b866_generalize_action.py @@ -0,0 +1,31 @@ +"""generalize_action + +Revision ID: 451e9507b866 +Revises: 2a75f0a06d90 +Create Date: 2013-03-29 12:13:33.303584 + +""" + +# revision identifiers, used by Alembic. +revision = "451e9507b866" +down_revision = "2a75f0a06d90" + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + ### commands auto generated by Alembic - please adjust! ### + op.add_column("action", sa.Column("message", sa.Text(), nullable=True)) + op.add_column("action", sa.Column("ended_on", sa.Integer(), nullable=True)) + op.drop_column("action", u"backend_message") + ### end Alembic commands ### + + +def downgrade(): + ### commands auto generated by Alembic - please adjust! ### + op.add_column( + "action", sa.Column(u"backend_message", sa.TEXT(), nullable=True)) + op.drop_column("action", "ended_on") + op.drop_column("action", "message") + ### end Alembic commands ### diff --git a/frontend/coprs_frontend/alembic/versions/4837ad1d96ea_drop_copr_build_coun.py b/frontend/coprs_frontend/alembic/versions/4837ad1d96ea_drop_copr_build_coun.py new file mode 100644 index 0000000..dc5e7ce --- /dev/null +++ b/frontend/coprs_frontend/alembic/versions/4837ad1d96ea_drop_copr_build_coun.py @@ -0,0 +1,24 @@ +"""drop Copr.build_count + +Revision ID: 4837ad1d96ea +Revises: 294405dfc7c0 +Create Date: 2014-01-20 17:05:20.917522 + +""" + +# revision identifiers, used by Alembic. +revision = "4837ad1d96ea" +down_revision = "294405dfc7c0" + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + """ Drop "build_count" colum from copr table. """ + op.drop_column("copr", "build_count") + + +def downgrade(): + """ Add "build_count" colum to copr table. """ + op.add_column("copr", sa.Column("build_count", sa.Integer(default=0))) diff --git a/frontend/coprs_frontend/alembic/versions/498884ac47db_add_timezone_field.py b/frontend/coprs_frontend/alembic/versions/498884ac47db_add_timezone_field.py new file mode 100644 index 0000000..add80d4 --- /dev/null +++ b/frontend/coprs_frontend/alembic/versions/498884ac47db_add_timezone_field.py @@ -0,0 +1,24 @@ +"""add timezone field + +Revision ID: 498884ac47db +Revises: 4837ad1d96ea +Create Date: 2014-01-23 12:15:04.450292 + +""" + +# revision identifiers, used by Alembic. +revision = '498884ac47db' +down_revision = '4837ad1d96ea' + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + """ Add 'data' colum to action table. """ + op.add_column('user', sa.Column('timezone', sa.String(length=50), nullable=True)) + + +def downgrade(): + """ Drop 'data' colum from action table. """ + op.drop_column('user', 'timezone') diff --git a/frontend/coprs_frontend/alembic/versions/52e53e7b413e_add_build_chroot.py b/frontend/coprs_frontend/alembic/versions/52e53e7b413e_add_build_chroot.py new file mode 100644 index 0000000..f2bbebe --- /dev/null +++ b/frontend/coprs_frontend/alembic/versions/52e53e7b413e_add_build_chroot.py @@ -0,0 +1,75 @@ +""" Add BuildChroot table + +Revision ID: 52e53e7b413e +Revises: 246fd2dbf398 +Create Date: 2013-11-14 09:00:43.787717 + +""" + +# revision identifiers, used by Alembic. +revision = "52e53e7b413e" +down_revision = "246fd2dbf398" + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + ### commands auto generated by Alembic - please adjust! ### + op.create_table("build_chroot", + sa.Column("mock_chroot_id", sa.Integer(), nullable=False), + sa.Column("build_id", sa.Integer(), nullable=False), + sa.Column("status", sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(["build_id"], ["build.id"], ), + sa.ForeignKeyConstraint( + ["mock_chroot_id"], ["mock_chroot.id"], ), + sa.PrimaryKeyConstraint("mock_chroot_id", "build_id") + ) + + # transfer data from build table to build_chroot + metadata = sa.MetaData() + # just what we need of copr table + build_table = sa.Table("build", metadata, + sa.Column("chroots", sa.Text()), + sa.Column("status", sa.Integer()), + sa.Column("id", sa.Integer()), + ) + + mc_table = sa.Table("mock_chroot", metadata, + sa.Column("id", sa.Integer(), nullable=False), + sa.Column( + "os_release", sa.String(length=50), nullable=False), + sa.Column( + "os_version", sa.String(length=50), nullable=False), + sa.Column( + "arch", sa.String(length=50), nullable=False), + sa.Column("is_active", sa.Boolean(), nullable=False), + ) + bc_table = sa.Table("build_chroot", metadata, + sa.Column( + "mock_chroot_id", sa.Integer(), nullable=False), + sa.Column("build_id", sa.Integer(), nullable=False), + sa.Column("status", sa.Integer(), nullable=True), + ) + for row in op.get_bind().execute(sa.select([build_table.c.id, build_table.c.chroots, build_table.c.status])): + for c in row[1].split(" "): + chroot_array = c.split("-") + for row2 in (op.get_bind().execute(sa.select([mc_table.c.id], sa.and_( + mc_table.c.os_release == op.inline_literal(chroot_array[0]), + mc_table.c.os_version == op.inline_literal(chroot_array[1]), + mc_table.c.arch == op.inline_literal(chroot_array[2]), + )))): # should be just one row + op.bulk_insert( + bc_table, [{"mock_chroot_id": row2[0], "build_id": row[0], "status": row[2]}]) + + # drop old columns + op.drop_column(u"build", u"status") + op.drop_column(u"build", u"chroots") + + +def downgrade(): + print "Why are you downgrading? You will just lost some data." + op.add_column(u"build", sa.Column(u"chroots", sa.TEXT(), nullable=False)) + op.add_column(u"build", sa.Column(u"status", sa.INTEGER(), nullable=True)) + op.drop_table("build_chroot") + print "Data about chroots for builds are gone!" diff --git a/frontend/coprs_frontend/alembic/versions/544873aa3ba1_add_action.py b/frontend/coprs_frontend/alembic/versions/544873aa3ba1_add_action.py new file mode 100644 index 0000000..dcacc6a --- /dev/null +++ b/frontend/coprs_frontend/alembic/versions/544873aa3ba1_add_action.py @@ -0,0 +1,40 @@ +"""empty message + +Revision ID: 544873aa3ba1 +Revises: 1ee4b45f5476 +Create Date: 2013-02-20 13:20:34.778470 + +""" + +# revision identifiers, used by Alembic. +revision = "544873aa3ba1" +down_revision = "1ee4b45f5476" + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + ### commands auto generated by Alembic - please adjust! ### + op.create_table("action", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("action_type", sa.Integer(), nullable=False), + sa.Column( + "object_type", sa.String(length=20), nullable=True), + sa.Column("object_id", sa.Integer(), nullable=True), + sa.Column( + "old_value", sa.String(length=255), nullable=True), + sa.Column( + "new_value", sa.String(length=255), nullable=True), + sa.Column("backend_result", sa.Integer(), nullable=True), + sa.Column("backend_message", sa.Text(), nullable=True), + sa.Column("created_on", sa.Integer(), nullable=True), + sa.PrimaryKeyConstraint("id") + ) + ### end Alembic commands ### + + +def downgrade(): + ### commands auto generated by Alembic - please adjust! ### + op.drop_table("action") + ### end Alembic commands ### diff --git a/frontend/coprs_frontend/alembic/versions/595a31c145fb_initial_db_setup.py b/frontend/coprs_frontend/alembic/versions/595a31c145fb_initial_db_setup.py new file mode 100644 index 0000000..857e14c --- /dev/null +++ b/frontend/coprs_frontend/alembic/versions/595a31c145fb_initial_db_setup.py @@ -0,0 +1,77 @@ +"""Initial DB setup + +Revision ID: 595a31c145fb +Revises: None +Create Date: 2012-11-26 09:39:51.229910 + +""" + +# revision identifiers, used by Alembic. +revision = "595a31c145fb" +down_revision = None + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + ### commands auto generated by Alembic - please adjust! ### + op.create_table("user", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column( + "openid_name", sa.String(length=100), nullable=False), + sa.Column("mail", sa.String(length=150), nullable=False), + sa.Column("proven", sa.Boolean(), nullable=True), + sa.Column("admin", sa.Boolean(), nullable=True), + sa.PrimaryKeyConstraint("id") + ) + op.create_table("copr", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("name", sa.String(length=100), nullable=False), + sa.Column("chroots", sa.Text(), nullable=False), + sa.Column("repos", sa.Text(), nullable=True), + sa.Column("created_on", sa.Integer(), nullable=True), + sa.Column("build_count", sa.Integer(), nullable=True), + sa.Column("owner_id", sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(["owner_id"], ["user.id"], ), + sa.PrimaryKeyConstraint("id") + ) + op.create_table("build", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("pkgs", sa.Text(), nullable=True), + sa.Column("canceled", sa.Boolean(), nullable=True), + sa.Column("chroots", sa.Text(), nullable=False), + sa.Column("repos", sa.Text(), nullable=True), + sa.Column("submitted_on", sa.Integer(), nullable=False), + sa.Column("started_on", sa.Integer(), nullable=True), + sa.Column("ended_on", sa.Integer(), nullable=True), + sa.Column("results", sa.Text(), nullable=True), + sa.Column("status", sa.Integer(), nullable=True), + sa.Column("memory_reqs", sa.Integer(), nullable=True), + sa.Column("timeout", sa.Integer(), nullable=True), + sa.Column("user_id", sa.Integer(), nullable=True), + sa.Column("copr_id", sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(["copr_id"], ["copr.id"], ), + sa.ForeignKeyConstraint(["user_id"], ["user.id"], ), + sa.PrimaryKeyConstraint("id") + ) + op.create_table("copr_permission", + sa.Column( + "copr_builder", sa.SmallInteger(), nullable=True), + sa.Column("copr_admin", sa.SmallInteger(), nullable=True), + sa.Column("user_id", sa.Integer(), nullable=False), + sa.Column("copr_id", sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(["copr_id"], ["copr.id"], ), + sa.ForeignKeyConstraint(["user_id"], ["user.id"], ), + sa.PrimaryKeyConstraint("user_id", "copr_id") + ) + ### end Alembic commands ### + + +def downgrade(): + ### commands auto generated by Alembic - please adjust! ### + op.drop_table("copr_permission") + op.drop_table("build") + op.drop_table("copr") + op.drop_table("user") + ### end Alembic commands ### diff --git a/frontend/coprs_frontend/alembic/versions/d062c3d9c00_backend_result_to_result.py b/frontend/coprs_frontend/alembic/versions/d062c3d9c00_backend_result_to_result.py new file mode 100644 index 0000000..f335648 --- /dev/null +++ b/frontend/coprs_frontend/alembic/versions/d062c3d9c00_backend_result_to_result.py @@ -0,0 +1,29 @@ +"""backend_result_to_result + +Revision ID: d062c3d9c00 +Revises: 451e9507b866 +Create Date: 2013-04-03 10:10:35.990681 + +""" + +# revision identifiers, used by Alembic. +revision = "d062c3d9c00" +down_revision = "451e9507b866" + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + ### commands auto generated by Alembic - please adjust! ### + op.add_column("action", sa.Column("result", sa.Integer(), nullable=True)) + op.drop_column("action", u"backend_result") + ### end Alembic commands ### + + +def downgrade(): + ### commands auto generated by Alembic - please adjust! ### + op.add_column( + "action", sa.Column(u"backend_result", sa.INTEGER(), nullable=True)) + op.drop_column("action", "result") + ### end Alembic commands ### diff --git a/frontend/coprs_frontend/application b/frontend/coprs_frontend/application new file mode 100755 index 0000000..817870e --- /dev/null +++ b/frontend/coprs_frontend/application @@ -0,0 +1,12 @@ +#!/usr/bin/python +import logging +import os +import sys + +# so that errors are not sent to stdout +logging.basicConfig(stream=sys.stderr) + +os.environ["COPRS_ENVIRON_PRODUCTION"] = "1" +sys.path.insert(0, os.path.dirname(__file__)) + +from coprs import app as application diff --git a/frontend/coprs_frontend/config/copr.conf b/frontend/coprs_frontend/config/copr.conf new file mode 100644 index 0000000..e608897 --- /dev/null +++ b/frontend/coprs_frontend/config/copr.conf @@ -0,0 +1,36 @@ +# Directory and files where is stored Copr database files +#DATA_DIR = '/var/lib/copr/data' +#DATABASE = '/var/lib/copr/data/copr.db' +#OPENID_STORE = '/var/lib/copr/data/openid_store' +#WHOOSHEE_DIR = '/var/lib/copr/data/whooshee' + +# salt for CSRF codes +#SECRET_KEY = 'put_some_secret_here' + +#BACKEND_PASSWORD = 'password_here' + +# restrict access to a set of users +#USE_ALLOWED_USERS = False +#ALLOWED_USERS = ['bonnie', 'clyde'] + +SQLALCHEMY_DATABASE_URI = 'postgresql+psycopg2://copr-fe:coprpass@/coprdb' + +# Token length, defaults to 30 (max 255) +#API_TOKEN_LENGTH = 30 + +# Expiration of API token in days +#API_TOKEN_EXPIRATION = 180 + +# logging options +#SEND_LOGS_TO = ['root@localhost'] +#LOGGING_LEVEL = logging.ERROR + +# where to send notice about raised legal flag +#SEND_LEGAL_TO = ['root@localhost', 'somebody@somewhere.com'] + +DEBUG = False +SQLALCHEMY_ECHO = False + +#CSRF_ENABLED = True +# as of Flask-WTF 0.9+ +#WTF_CSRF_ENABLED = True diff --git a/frontend/coprs_frontend/config/copr_devel.conf b/frontend/coprs_frontend/config/copr_devel.conf new file mode 100644 index 0000000..2f139d1 --- /dev/null +++ b/frontend/coprs_frontend/config/copr_devel.conf @@ -0,0 +1,33 @@ +# Directory and files where is stored Copr database files +#DATA_DIR = '/var/lib/copr/data' +#DATABASE = '/var/lib/copr/data/copr.db' +#OPENID_STORE = '/var/lib/copr/data/openid_store' +#WHOOSHEE_DIR = '/var/lib/copr/data/whooshee' + +# salt for CSRF codes +#SECRET_KEY = 'put_some_secret_here' + +#BACKEND_PASSWORD = 'password_here' + +# restrict access to a set of users +#USE_ALLOWED_USERS = False +#ALLOWED_USERS = ['bonnie', 'clyde'] + +#SQLALCHEMY_DATABASE_URI = 'sqlite:////var/lib/copr/data/copr.db' + +# Token length, defaults to 30 (max 255) +#API_TOKEN_LENGTH = 30 + +# Expiration of API token in days +#API_TOKEN_EXPIRATION = 180 + +# logging options +#SEND_LOGS_TO = ['root@localhost'] +#LOGGING_LEVEL = logging.ERROR + +DEBUG = True +SQLALCHEMY_ECHO = True + +#CSRF_ENABLED = True +# as of Flask-WTF 0.9+ +#WTF_CSRF_ENABLED = True diff --git a/frontend/coprs_frontend/config/copr_unit_test.conf b/frontend/coprs_frontend/config/copr_unit_test.conf new file mode 100644 index 0000000..e9de359 --- /dev/null +++ b/frontend/coprs_frontend/config/copr_unit_test.conf @@ -0,0 +1,33 @@ +# Directory and files where is stored Copr database files +DATA_DIR = '/tmp' +DATABASE = '/tmp/copr.db' +OPENID_STORE = '/tmp/openid_store' +WHOOSHEE_DIR = '/tmp/whooshee' + +# salt for CSRF codes +#SECRET_KEY = 'put_some_secret_here' + +#BACKEND_PASSWORD = 'password_here' + +# restrict access to a set of users +#USE_ALLOWED_USERS = False +#ALLOWED_USERS = ['bonnie', 'clyde'] + +SQLALCHEMY_DATABASE_URI = 'sqlite:///' + DATABASE + +# Token length, defaults to 30 (max 255) +#API_TOKEN_LENGTH = 30 + +# Expiration of API token in days +#API_TOKEN_EXPIRATION = 180 + +# logging options +#SEND_LOGS_TO = ['root@localhost'] +#LOGGING_LEVEL = logging.ERROR + +#DEBUG = False +#SQLALCHEMY_ECHO = False + +CSRF_ENABLED = False +# as of Flask-WTF 0.9+ +WTF_CSRF_ENABLED = False diff --git a/frontend/coprs_frontend/coprs.conf.example b/frontend/coprs_frontend/coprs.conf.example new file mode 100644 index 0000000..7162d5f --- /dev/null +++ b/frontend/coprs_frontend/coprs.conf.example @@ -0,0 +1,20 @@ + + ServerName 127.0.0.1 + + #WSGIPassAuthorization On + WSGIDaemonProcess 127.0.0.1 user=copr-fe group=copr-fe threads=5 + WSGIScriptAlias / /usr/share/copr/coprs_frontend/application + WSGIProcessGroup 127.0.0.1 + + ErrorLog logs/error_coprs + CustomLog logs/access_coprs common + + + WSGIApplicationGroup %{GLOBAL} + # apache 2.2 (el6, F17) + #Order deny,allow + #Allow from all + # apache 2.4 (F18+) + Require all granted + + diff --git a/frontend/coprs_frontend/coprs/__init__.py b/frontend/coprs_frontend/coprs/__init__.py new file mode 100644 index 0000000..63192ab --- /dev/null +++ b/frontend/coprs_frontend/coprs/__init__.py @@ -0,0 +1,51 @@ +from __future__ import with_statement + +import os +import flask + +from flask.ext.sqlalchemy import SQLAlchemy +from flask.ext.openid import OpenID +from flask.ext.whooshee import Whooshee + +app = flask.Flask(__name__) + +if "COPRS_ENVIRON_PRODUCTION" in os.environ: + app.config.from_object("coprs.config.ProductionConfig") +elif "COPRS_ENVIRON_UNITTEST" in os.environ: + app.config.from_object("coprs.config.UnitTestConfig") +else: + app.config.from_object("coprs.config.DevelopmentConfig") +if os.environ.get("COPR_CONFIG"): + app.config.from_envvar("COPR_CONFIG") +else: + app.config.from_pyfile("/etc/copr/copr.conf", silent=True) + + +oid = OpenID(app, app.config["OPENID_STORE"]) +db = SQLAlchemy(app) +whooshee = Whooshee(app) + +import coprs.filters +import coprs.log +import coprs.models +import coprs.whoosheers + +from coprs.views import admin_ns +from coprs.views.admin_ns import admin_general +from coprs.views import api_ns +from coprs.views.api_ns import api_general +from coprs.views import coprs_ns +from coprs.views.coprs_ns import coprs_builds +from coprs.views.coprs_ns import coprs_general +from coprs.views.coprs_ns import coprs_chroots +from coprs.views import backend_ns +from coprs.views.backend_ns import backend_general +from coprs.views import misc + +app.register_blueprint(api_ns.api_ns) +app.register_blueprint(admin_ns.admin_ns) +app.register_blueprint(coprs_ns.coprs_ns) +app.register_blueprint(misc.misc) +app.register_blueprint(backend_ns.backend_ns) + +app.add_url_rule("/", "coprs_ns.coprs_show", coprs_general.coprs_show) diff --git a/frontend/coprs_frontend/coprs/config.py b/frontend/coprs_frontend/coprs/config.py new file mode 100644 index 0000000..31b3618 --- /dev/null +++ b/frontend/coprs_frontend/coprs/config.py @@ -0,0 +1,52 @@ +import os +import logging + + +class Config(object): + DATA_DIR = os.path.join(os.path.dirname(__file__), "../../data") + DATABASE = os.path.join(DATA_DIR, "copr.db") + OPENID_STORE = os.path.join(DATA_DIR, "openid_store") + WHOOSHEE_DIR = os.path.join(DATA_DIR, "whooshee") + SECRET_KEY = "THISISNOTASECRETATALL" + BACKEND_PASSWORD = "thisisbackend" + + # restrict access to a set of users + USE_ALLOWED_USERS = False + ALLOWED_USERS = [] + + # SQLAlchemy + SQLALCHEMY_DATABASE_URI = "sqlite:///" + os.path.abspath(DATABASE) + + # Token length, defaults to 30, DB set to varchar 255 + API_TOKEN_LENGTH = 30 + + # Expiration of API token in days + API_TOKEN_EXPIRATION = 180 + + # logging options + SEND_LOGS_TO = ["root@localhost"] + LOGGING_LEVEL = logging.ERROR + + SEND_LEGAL_TO = ["root@localhost"] + + +class ProductionConfig(Config): + DEBUG = False + #SECRET_KEY = "put_some_secret_here" + #BACKEND_PASSWORD = "password_here" + #SQLALCHEMY_DATABASE_URI = "postgresql+psycopg2://login:password@/db_name" + + +class DevelopmentConfig(Config): + DEBUG = True + SQLALCHEMY_ECHO = True + + +class UnitTestConfig(Config): + CSRF_ENABLED = False + DATABASE = os.path.abspath("tests/data/copr.db") + OPENID_STORE = os.path.abspath("tests/data/openid_store") + WHOOSHEE_DIR = os.path.abspath("tests/data/whooshee") + + # SQLAlchemy + SQLALCHEMY_DATABASE_URI = "sqlite:///" + os.path.abspath(DATABASE) diff --git a/frontend/coprs_frontend/coprs/constants.py b/frontend/coprs_frontend/coprs/constants.py new file mode 100644 index 0000000..a0709e0 --- /dev/null +++ b/frontend/coprs_frontend/coprs/constants.py @@ -0,0 +1,27 @@ +# Settings for chroots +INTEL_ARCHES = ["i386", "x86_64"] +DEFAULT_ARCHES = INTEL_ARCHES + +CHROOTS = { + "fedora-17": DEFAULT_ARCHES, + "fedora-18": DEFAULT_ARCHES, + "fedora-19": DEFAULT_ARCHES, + "fedora-20": DEFAULT_ARCHES, + "fedora-rawhide": DEFAULT_ARCHES, + "epel-5": DEFAULT_ARCHES, + "epel-6": DEFAULT_ARCHES, +} + +# PAGINATION +ITEMS_PER_PAGE = 10 +PAGES_URLS_COUNT = 5 + +# Builds defaults +## memory in MB +DEFAULT_BUILD_MEMORY = 2048 +MIN_BUILD_MEMORY = 2048 +MAX_BUILD_MEMORY = 4096 +# in seconds +DEFAULT_BUILD_TIMEOUT = 0 +MIN_BUILD_TIMEOUT = 0 +MAX_BUILD_TIMEOUT = 36000 diff --git a/frontend/coprs_frontend/coprs/exceptions.py b/frontend/coprs_frontend/coprs/exceptions.py new file mode 100644 index 0000000..8806c1c --- /dev/null +++ b/frontend/coprs_frontend/coprs/exceptions.py @@ -0,0 +1,34 @@ +class ArgumentMissingException(BaseException): + pass + + +class MalformedArgumentException(ValueError): + pass + + +class NotFoundException(BaseException): + pass + + +class DuplicateException(BaseException): + pass + + +class InsufficientRightsException(BaseException): + pass + + +class ActionInProgressException(BaseException): + + def __init__(self, msg, action): + self.msg = msg + self.action = action + + def __unicode__(self): + return self.formatted_msg() + + def __str__(self): + return self.__unicode__() + + def formatted_msg(self): + return self.msg.format(action=self.action) diff --git a/frontend/coprs_frontend/coprs/filters.py b/frontend/coprs_frontend/coprs/filters.py new file mode 100644 index 0000000..6a3432e --- /dev/null +++ b/frontend/coprs_frontend/coprs/filters.py @@ -0,0 +1,66 @@ +import datetime +import pytz +import time +import markdown + +from flask import Markup + +from coprs import app +from coprs import helpers + + +@app.template_filter("date_from_secs") +def date_from_secs(secs): + if secs: + return time.strftime("%Y-%m-%d %H:%M:%S %Z", time.gmtime(secs)) + + return None + + +@app.template_filter("perm_type_from_num") +def perm_type_from_num(num): + return helpers.PermissionEnum(num) + + +@app.template_filter("os_name_short") +def os_name_short(os_name, os_version): + # TODO: make it models.MockChroot method or not? + if os_version: + if os_version == "rawhide": + return os_version + if os_name == "fedora": + return "fc.{0}".format(os_version) + elif os_name == "epel": + return "el{0}".format(os_version) + return os_name + + +@app.template_filter('localized_time') +def localized_time(time_in, timezone): + """ return time shifted into timezone (and printed in ISO format) + + Input is in EPOCH (seconds since epoch). + """ + if not time_in: + return "Not yet" + format_tz = "%Y-%m-%d %H:%M:%S %Z" + utc_tz = pytz.timezone('UTC') + if timezone: + user_tz = pytz.timezone(timezone) + else: + user_tz = utc_tz + dt_aware = datetime.datetime.fromtimestamp(time_in).replace(tzinfo=utc_tz) + dt_my_tz = dt_aware.astimezone(user_tz) + return dt_my_tz.strftime(format_tz) + + +@app.template_filter("markdown") +def markdown_filter(data): + if not data: + return '' + + md = markdown.Markdown( + safe_mode="replace", + html_replacement_text="--RAW HTML NOT ALLOWED--") + + return Markup(md.convert(data)) diff --git a/frontend/coprs_frontend/coprs/forms.py b/frontend/coprs_frontend/coprs/forms.py new file mode 100644 index 0000000..6d77974 --- /dev/null +++ b/frontend/coprs_frontend/coprs/forms.py @@ -0,0 +1,281 @@ +import re +import urlparse + +import flask +import wtforms + +from flask.ext import wtf + +from coprs import constants +from coprs import helpers +from coprs import models +from coprs.logic import coprs_logic + + +class UrlListValidator(object): + + def __init__(self, message=None): + if not message: + message = "A list of URLs separated by whitespace characters" + " is needed ('{0}' doesn't seem to be a URL)." + self.message = message + + def __call__(self, form, field): + urls = field.data.split() + for u in urls: + if not self.is_url(u): + raise wtforms.ValidationError(self.message.format(u)) + + def is_url(self, url): + parsed = urlparse.urlparse(url) + is_url = True + + if not parsed.scheme.startswith("http"): + is_url = False + if not parsed.netloc: + is_url = False + + return is_url + + +class CoprUniqueNameValidator(object): + + def __init__(self, message=None): + if not message: + message = "You already have project named '{0}'." + self.message = message + + def __call__(self, form, field): + existing = coprs_logic.CoprsLogic.exists_for_user( + flask.g.user, field.data).first() + + if existing and str(existing.id) != form.id.data: + raise wtforms.ValidationError(self.message.format(field.data)) + + +class StringListFilter(object): + + def __call__(self, value): + if not value: + return '' + # Replace every whitespace string with one newline + # Formats ideally for html form filling, use replace('\n', ' ') + # to get space-separated values or split() to get list + result = value.strip() + regex = re.compile(r"\s+") + return regex.sub(lambda x: '\n', result) + + +class ValueToPermissionNumberFilter(object): + + def __call__(self, value): + if value: + return helpers.PermissionEnum("request") + return helpers.PermissionEnum("nothing") + + +class CoprFormFactory(object): + + @staticmethod + def create_form_cls(mock_chroots=None): + class F(wtf.Form): + # also use id here, to be able to find out whether user + # is updating a copr if so, we don't want to shout + # that name already exists + id = wtforms.HiddenField() + + name = wtforms.TextField( + "Name", + validators=[ + wtforms.validators.Required(), + wtforms.validators.Regexp( + re.compile(r"^[\w.-]+$"), + message="Name must contain only letters," + "digits, underscores, dashes and dots."), + CoprUniqueNameValidator() + ]) + + description = wtforms.TextAreaField("Description") + + instructions = wtforms.TextAreaField("Instructions") + + repos = wtforms.TextAreaField( + "Repos", + validators=[UrlListValidator()], + filters=[StringListFilter()]) + + initial_pkgs = wtforms.TextAreaField( + "Initial packages to build", + validators=[UrlListValidator()], + filters=[StringListFilter()]) + + @property + def selected_chroots(self): + selected = [] + for ch in self.chroots_list: + if getattr(self, ch).data: + selected.append(ch) + return selected + + def validate(self): + if not super(F, self).validate(): + return False + + if not self.validate_mock_chroots_not_empty(): + self._mock_chroots_error = "At least one chroot" \ + " must be selected" + return False + return True + + def validate_mock_chroots_not_empty(self): + have_any = False + for c in self.chroots_list: + if getattr(self, c).data: + have_any = True + return have_any + + F.chroots_list = map(lambda x: x.name, + models.MockChroot.query.filter( + models.MockChroot.is_active == True + ).all()) + F.chroots_list.sort() + # sets of chroots according to how we should print them in columns + F.chroots_sets = {} + for ch in F.chroots_list: + checkbox_default = False + if mock_chroots and ch in map(lambda x: x.name, + mock_chroots): + checkbox_default = True + + setattr(F, ch, wtforms.BooleanField(ch, default=checkbox_default)) + if ch[0] in F.chroots_sets: + F.chroots_sets[ch[0]].append(ch) + else: + F.chroots_sets[ch[0]] = [ch] + + return F + + +class CoprDeleteForm(wtf.Form): + verify = wtforms.TextField( + "Confirm deleting by typing 'yes'", + validators=[ + wtforms.validators.Required(), + wtforms.validators.Regexp( + r"^yes$", + message="Type 'yes' - without the quotes, lowercase.") + ]) + + +class BuildForm(wtf.Form): + pkgs = wtforms.TextAreaField( + "Pkgs", + validators=[ + wtforms.validators.Required(), + UrlListValidator()], + filters=[StringListFilter()]) + + memory_reqs = wtforms.IntegerField( + "Memory requirements", + validators=[ + wtforms.validators.NumberRange( + min=constants.MIN_BUILD_MEMORY, + max=constants.MAX_BUILD_MEMORY)], + default=constants.DEFAULT_BUILD_MEMORY) + + timeout = wtforms.IntegerField( + "Timeout", + validators=[ + wtforms.validators.NumberRange( + min=constants.MIN_BUILD_TIMEOUT, + max=constants.MAX_BUILD_TIMEOUT)], + default=constants.DEFAULT_BUILD_TIMEOUT) + + +class ChrootForm(wtf.Form): + + """ + Validator for editing chroots in project + (adding packages to minimal chroot) + """ + + buildroot_pkgs = wtforms.TextField( + "Additional packages to be always present in minimal buildroot") + + +class CoprLegalFlagForm(wtf.Form): + comment = wtforms.TextAreaField("Comment") + + +class PermissionsApplierFormFactory(object): + + @staticmethod + def create_form_cls(permission=None): + class F(wtf.Form): + pass + + builder_default = False + admin_default = False + + if permission: + if permission.copr_builder != helpers.PermissionEnum("nothing"): + builder_default = True + if permission.copr_admin != helpers.PermissionEnum("nothing"): + admin_default = True + + setattr(F, "copr_builder", + wtforms.BooleanField( + default=builder_default, + filters=[ValueToPermissionNumberFilter()])) + + setattr(F, "copr_admin", + wtforms.BooleanField( + default=admin_default, + filters=[ValueToPermissionNumberFilter()])) + + return F + + +class PermissionsFormFactory(object): + + """Creates a dynamic form for given set of copr permissions""" + @staticmethod + def create_form_cls(permissions): + class F(wtf.Form): + pass + + for perm in permissions: + builder_choices = helpers.PermissionEnum.choices_list() + admin_choices = helpers.PermissionEnum.choices_list() + + builder_default = perm.copr_builder + admin_default = perm.copr_admin + + setattr(F, "copr_builder_{0}".format(perm.user.id), + wtforms.SelectField( + choices=builder_choices, + default=builder_default, + coerce=int)) + + setattr(F, "copr_admin_{0}".format(perm.user.id), + wtforms.SelectField( + choices=admin_choices, + default=admin_default, + coerce=int)) + + return F + +class CoprModifyForm(wtf.Form): + description = wtforms.TextAreaField('Description', + validators=[wtforms.validators.Optional()]) + + instructions = wtforms.TextAreaField('Instructions', + validators=[wtforms.validators.Optional()]) + + repos = wtforms.TextAreaField('Repos', + validators=[UrlListValidator(), + wtforms.validators.Optional()], + filters=[StringListFilter()]) + +class ModifyChrootForm(wtf.Form): + buildroot_pkgs = wtforms.TextField('Additional packages to be always present in minimal buildroot') diff --git a/frontend/coprs_frontend/coprs/helpers.py b/frontend/coprs_frontend/coprs/helpers.py new file mode 100644 index 0000000..2074bdb --- /dev/null +++ b/frontend/coprs_frontend/coprs/helpers.py @@ -0,0 +1,227 @@ +import math +import random +import string +import urlparse +import flask + +from coprs import constants + +from rpmUtils.miscutils import splitFilename + + +def generate_api_token(size=30): + """ Generate a random string used as token to access the API + remotely. + + :kwarg: size, the size of the token to generate, defaults to 30 + chars. + :return: a string, the API token for the user. + """ + return ''.join(random.choice(string.ascii_lowercase) for x in range(size)) + + +class EnumType(type): + + def __call__(self, attr): + if isinstance(attr, int): + for k, v in self.vals.items(): + if v == attr: + return k + raise KeyError("num {0} is not mapped".format(attr)) + else: + return self.vals[attr] + + +class PermissionEnum(object): + __metaclass__ = EnumType + vals = {"nothing": 0, "request": 1, "approved": 2} + + @classmethod + def choices_list(cls, without=-1): + return [(n, k) for k, n in cls.vals.items() if n != without] + + +class ActionTypeEnum(object): + __metaclass__ = EnumType + vals = {"delete": 0, "rename": 1, "legal-flag": 2} + + +class BackendResultEnum(object): + __metaclass__ = EnumType + vals = {"waiting": 0, "success": 1, "failure": 2} + + +class RoleEnum(object): + __metaclass__ = EnumType + vals = {"user": 0, "admin": 1} + + +class StatusEnum(object): + __metaclass__ = EnumType + vals = {"failed": 0, + "succeeded": 1, + "canceled": 2, + "running": 3, + "pending": 4} + + +class Paginator(object): + + def __init__(self, query, total_count, page=1, + per_page_override=None, urls_count_override=None): + + self.query = query + self.total_count = total_count + self.page = page + self.per_page = per_page_override or constants.ITEMS_PER_PAGE + self.urls_count = urls_count_override or constants.PAGES_URLS_COUNT + self._sliced_query = None + + def page_slice(self, page): + return (constants.ITEMS_PER_PAGE * (page - 1), + constants.ITEMS_PER_PAGE * page) + + @property + def sliced_query(self): + if not self._sliced_query: + self._sliced_query = self.query[slice(*self.page_slice(self.page))] + return self._sliced_query + + @property + def pages(self): + return int(math.ceil(self.total_count / float(self.per_page))) + + def border_url(self, request, start): + if start: + if self.page - 1 > self.urls_count / 2: + return (self.url_for_other_page(request, 1), 1) + else: + if self.page < self.pages - self.urls_count / 2: + return (self.url_for_other_page(request, self.pages), + self.pages) + + return None + + def get_urls(self, request): + left_border = self.page - self.urls_count / 2 + left_border = 1 if left_border < 1 else left_border + right_border = self.page + self.urls_count / 2 + right_border = self.pages if right_border > self.pages else right_border + + return [(self.url_for_other_page(request, i), i) + for i in range(left_border, right_border + 1)] + + def url_for_other_page(self, request, page): + args = request.view_args.copy() + args["page"] = page + return flask.url_for(request.endpoint, **args) + + +def parse_package_name(pkg): + """ + Parse package name from possibly incomplete nvra string. + """ + + if pkg.count(".") >= 3 and pkg.count("-") >= 2: + return splitFilename(pkg)[0] + + # doesn"t seem like valid pkg string, try to guess package name + result = "" + pkg = pkg.replace(".rpm", "").replace(".src", "") + + for delim in ["-", "."]: + if delim in pkg: + parts = pkg.split(delim) + for part in parts: + if any(map(lambda x: x.isdigit(), part)): + return result[:-1] + + result += part + "-" + + return result[:-1] + + return pkg + + +def render_repo(copr, mock_chroot, url): + """ Render .repo file. No checks if copr or mock_chroot exists. """ + if mock_chroot.os_release == "fedora": + if mock_chroot.os_version != "rawhide": + mock_chroot.os_version = "$releasever" + + url = urlparse.urljoin( + url, "{0}-{1}-{2}/".format(mock_chroot.os_release, + mock_chroot.os_version, "$basearch")) + + #url = url.replace("http://", "https://") + return flask.render_template("coprs/copr.repo", copr=copr, url=url) + + +class Serializer(object): + + def to_dict(self, options={}): + """ + Usage: + + SQLAlchObject.to_dict() => returns a flat dict of the object + SQLAlchObject.to_dict({"foo": {}}) => returns a dict of the object + and will include a flat dict of object foo inside of that + SQLAlchObject.to_dict({"foo": {"bar": {}}, "spam": {}}) => returns + a dict of the object, which will include dict of foo + (which will include dict of bar) and dict of spam. + + Options can also contain two special values: __columns_only__ + and __columns_except__ + + If present, the first makes only specified fiels appear, + the second removes specified fields. Both of these fields + must be either strings (only works for one field) or lists + (for one and more fields). + + SQLAlchObject.to_dict({"foo": {"__columns_except__": ["id"]}, + "__columns_only__": "name"}) => + + The SQLAlchObject will only put its "name" into the resulting dict, + while "foo" all of its fields except "id". + + Options can also specify whether to include foo_id when displaying + related foo object (__included_ids__, defaults to True). + This doesn"t apply when __columns_only__ is specified. + """ + + result = {} + columns = self.serializable_attributes + + if "__columns_only__" in options: + columns = options["__columns_only__"] + else: + columns = set(columns) + if "__columns_except__" in options: + columns_except = options["__columns_except__"] + if not isinstance(options["__columns_except__"], list): + columns_except = [options["__columns_except__"]] + + columns -= set(columns_except) + + if ("__included_ids__" in options and + options["__included_ids__"] is False): + + related_objs_ids = [ + r + "_id" for r, o in options.items() + if not r.startswith("__")] + + columns -= set(related_objs_ids) + + columns = list(columns) + + for column in columns: + result[column] = getattr(self, column) + + for related, values in options.items(): + if hasattr(self, related): + result[related] = getattr(self, related).to_dict(values) + return result + + @property + def serializable_attributes(self): + return map(lambda x: x.name, self.__table__.columns) diff --git a/frontend/coprs_frontend/coprs/log.py b/frontend/coprs_frontend/coprs/log.py new file mode 100644 index 0000000..2f05da3 --- /dev/null +++ b/frontend/coprs_frontend/coprs/log.py @@ -0,0 +1,31 @@ +import logging +import logging.handlers + +from coprs import app + +send_logs_to = app.config.get("SEND_LOGS_TO") +level = app.config.get("LOGGING_LEVEL") + +formatter = logging.Formatter(""" +Message type: %(levelname)s +Location: %(pathname)s:%(lineno)d +Module: %(module)s +Function: %(funcName)s +Time: %(asctime)s + +Message: + +%(message)s +""") + +if not app.debug: + mail_handler = logging.handlers.SMTPHandler( + "127.0.0.1", + "copr-fe-error@{0}".format( + app.config["SERVER_NAME"] or "fedorahosted.org"), + send_logs_to, + "Yay, error in copr frontend occured!") + + mail_handler.setFormatter(formatter) + mail_handler.setLevel(level) + app.logger.addHandler(mail_handler) diff --git a/frontend/coprs_frontend/coprs/logic/__init__.py b/frontend/coprs_frontend/coprs/logic/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/frontend/coprs_frontend/coprs/logic/__init__.py diff --git a/frontend/coprs_frontend/coprs/logic/actions_logic.py b/frontend/coprs_frontend/coprs/logic/actions_logic.py new file mode 100644 index 0000000..b77079e --- /dev/null +++ b/frontend/coprs_frontend/coprs/logic/actions_logic.py @@ -0,0 +1,53 @@ +from coprs import db +from coprs import models +from coprs import helpers + + +class ActionsLogic(object): + + @classmethod + def get(cls, action_id): + """ + Return single action identified by `action_id` + """ + + query = models.Action.query.filter(models.Action.id == action_id) + return query + + @classmethod + def get_waiting(cls): + """ + Return actions that aren't finished + """ + + query = (models.Action.query + .filter(models.Action.result == + helpers.BackendResultEnum("waiting")) + .filter(models.Action.action_type != + helpers.ActionTypeEnum("legal-flag")) + .order_by(models.Action.created_on.asc())) + + return query + + @classmethod + def get_by_ids(cls, ids): + """ + Return actions matching passed `ids` + """ + + return models.Action.query.filter(models.Action.id.in_(ids)) + + @classmethod + def update_state_from_dict(cls, action, upd_dict): + """ + Update `action` object with `upd_dict` data + + Updates result, message and ended_on parameters. + """ + + for attr in ["result", "message", "ended_on"]: + value = upd_dict.get(attr, None) + if value: + setattr(action, attr, value) + + db.session.add(action) diff --git a/frontend/coprs_frontend/coprs/logic/builds_logic.py b/frontend/coprs_frontend/coprs/logic/builds_logic.py new file mode 100644 index 0000000..a6e234c --- /dev/null +++ b/frontend/coprs_frontend/coprs/logic/builds_logic.py @@ -0,0 +1,183 @@ +import time + +from coprs import db +from coprs import exceptions +from coprs import models +from coprs import helpers +from coprs import signals + +from coprs.logic import coprs_logic +from coprs.logic import users_logic + + +class BuildsLogic(object): + + @classmethod + def get(cls, build_id): + query = models.Build.query.filter(models.Build.id == build_id) + return query + + @classmethod + def get_multiple(cls, user, **kwargs): + copr = kwargs.get("copr", None) + username = kwargs.get("username", None) + coprname = kwargs.get("coprname", None) + + query = models.Build.query.order_by(models.Build.submitted_on.desc()) + + # if we get copr, query by its id + if copr: + query = query.filter(models.Build.copr == copr) + elif username and coprname: + query = (query.join(models.Build.copr) + .options(db.contains_eager(models.Build.copr)) + .join(models.Copr.owner) + .filter(models.Copr.name == coprname) + .filter(models.User.openid_name == + models.User.openidize_name(username)) + .order_by(models.Build.submitted_on.desc())) + else: + raise exceptions.ArgumentMissingException( + "Must pass either copr or both coprname and username") + + return query + + @classmethod + def get_waiting(cls): + """ + Return builds that aren't both started and finished + (if build start submission fails, we still want to mark + the build as non-waiting, if it ended) + this has very different goal then get_multiple, so implement it alone + """ + + query = (models.Build.query.join(models.Build.copr) + .join(models.User) + .options(db.contains_eager(models.Build.copr)) + .options(db.contains_eager("copr.owner")) + .filter((models.Build.started_on == None) + | (models.Build.started_on < int(time.time() - 7200))) + .filter(models.Build.ended_on == None) + .filter(models.Build.canceled != True) + .order_by(models.Build.submitted_on.asc())) + return query + + @classmethod + def get_by_ids(cls, ids): + return models.Build.query.filter(models.Build.id.in_(ids)) + + @classmethod + def add(cls, user, pkgs, copr, + repos=None, memory_reqs=None, timeout=None): + + coprs_logic.CoprsLogic.raise_if_unfinished_blocking_action( + user, copr, + "Can't build while there is an operation in progress: {action}") + users_logic.UsersLogic.raise_if_cant_build_in_copr( + user, copr, + "You don't have permissions to build in this copr.") + + if not repos: + repos = copr.repos + + build = models.Build( + user=user, + pkgs=pkgs, + copr=copr, + repos=repos, + submitted_on=int(time.time())) + + if memory_reqs: + build.memory_reqs = memory_reqs + + if timeout: + build.timeout = timeout + + db.session.add(build) + + # add BuildChroot object for each active chroot + # this copr is assigned to + for chroot in copr.active_chroots: + buildchroot = models.BuildChroot( + build=build, + mock_chroot=chroot) + + db.session.add(buildchroot) + + return build + + @classmethod + def update_state_from_dict(cls, build, upd_dict): + if "chroot" in upd_dict: + # update respective chroot status + for build_chroot in build.build_chroots: + if build_chroot.name == upd_dict["chroot"]: + if "status" in upd_dict: + build_chroot.status = upd_dict["status"] + + db.session.add(build_chroot) + + for attr in ["results", "started_on", "ended_on"]: + value = upd_dict.get(attr, None) + if value: + # only update started_on once + if attr == "started_on" and build.started_on: + continue + + # only update ended_on and results + # when there are no pending builds + if (attr in ["ended_on", "results"] and + build.has_pending_chroot): + continue + + if attr == "ended_on": + signals.build_finished.send(cls, build=build) + + setattr(build, attr, value) + + db.session.add(build) + + @classmethod + def cancel_build(cls, user, build): + if not (user.can_build_in(build.copr)): + raise exceptions.InsufficientRightsException( + "You are not allowed to cancel this build.") + build.canceled = True + + @classmethod + def delete_build(cls, user, build): + if not (user.can_build_in(build.copr)): + raise exceptions.InsufficientRightsException( + "You are not allowed to delete this build.") + + action = models.Action(action_type=helpers.ActionTypeEnum("delete"), + object_type="build", + object_id=build.id, + old_value="{0}/{1}".format(build.copr.owner.name, + build.copr.name), + data=build.pkgs, + created_on=int(time.time())) + + db.session.add(action) + for build_chroot in build.build_chroots: + db.session.delete(build_chroot) + db.session.delete(build) + + @classmethod + def last_modified(cls, copr): + """ Get build datetime (as epoch) of last successfull build + + :arg copr: object of copr + """ + builds = cls.get_multiple(None, copr=copr) + + last_build = (builds + .join(models.BuildChroot) + .filter(models.BuildChroot.status == helpers.StatusEnum("succeeded")) + .filter(models.Build.ended_on != None) + .order_by(models.Build.ended_on.desc()) + ).first() + if last_build: + return last_build.ended_on + else: + return None diff --git a/frontend/coprs_frontend/coprs/logic/coprs_logic.py b/frontend/coprs_frontend/coprs/logic/coprs_logic.py new file mode 100644 index 0000000..7cab264 --- /dev/null +++ b/frontend/coprs_frontend/coprs/logic/coprs_logic.py @@ -0,0 +1,435 @@ +import time + +from coprs import db +from coprs import exceptions +from coprs import helpers +from coprs import models +from coprs import signals +from coprs.logic import users_logic + +class CoprsLogic(object): + + """ + Used for manipulating Coprs. + + All methods accept user object as a first argument, + as this may be needed in future. + """ + + @classmethod + def get(cls, user, username, coprname, **kwargs): + with_builds = kwargs.get("with_builds", False) + with_mock_chroots = kwargs.get("with_mock_chroots", False) + + query = (db.session.query(models.Copr) + .join(models.Copr.owner) + .options(db.contains_eager(models.Copr.owner)) + .filter(models.Copr.name == coprname) + .filter(models.User.openid_name == + models.User.openidize_name(username)) + .filter(models.Copr.deleted == False)) + + if with_builds: + query = (query.outerjoin(models.Copr.builds) + .options(db.contains_eager(models.Copr.builds)) + .order_by(models.Build.submitted_on.desc())) + + if with_mock_chroots: + query = (query.outerjoin(*models.Copr.mock_chroots.attr) + .options(db.contains_eager(*models.Copr.mock_chroots.attr)) + .order_by(models.MockChroot.os_release.asc()) + .order_by(models.MockChroot.os_version.asc()) + .order_by(models.MockChroot.arch.asc())) + + return query + + @classmethod + def get_multiple(cls, user, **kwargs): + user_relation = kwargs.get("user_relation", None) + username = kwargs.get("username", None) + with_mock_chroots = kwargs.get("with_mock_chroots", None) + with_builds = kwargs.get("with_builds", None) + incl_deleted = kwargs.get("incl_deleted", None) + ids = kwargs.get("ids", None) + + query = (db.session.query(models.Copr) + .join(models.Copr.owner) + .options(db.contains_eager(models.Copr.owner)) + .order_by(models.Copr.id.desc())) + + if not incl_deleted: + query = query.filter(models.Copr.deleted == False) + + if isinstance(ids, list): # can be an empty list + query = query.filter(models.Copr.id.in_(ids)) + + if user_relation == "owned": + query = query.filter( + models.User.openid_name == models.User.openidize_name(username)) + elif user_relation == "allowed": + aliased_user = db.aliased(models.User) + + query = (query.join(models.CoprPermission, + models.Copr.copr_permissions) + .filter(models.CoprPermission.copr_builder == + helpers.PermissionEnum('approved')) + .join(aliased_user, models.CoprPermission.user) + .filter(aliased_user.openid_name == + models.User.openidize_name(username))) + + if with_mock_chroots: + query = (query.outerjoin(*models.Copr.mock_chroots.attr) + .options(db.contains_eager(*models.Copr.mock_chroots.attr)) + .order_by(models.MockChroot.os_release.asc()) + .order_by(models.MockChroot.os_version.asc()) + .order_by(models.MockChroot.arch.asc())) + + if with_builds: + query = (query.outerjoin(models.Copr.builds) + .options(db.contains_eager(models.Copr.builds)) + .order_by(models.Build.submitted_on.desc())) + + return query + + @classmethod + def get_multiple_fulltext(cls, user, search_string): + query = (models.Copr.query.join(models.User) + .filter(models.Copr.deleted == False) + .whooshee_search(search_string)) + return query + + @classmethod + def add(cls, user, name, repos, selected_chroots, description, + instructions, check_for_duplicates=False): + copr = models.Copr(name=name, + repos=repos, + owner=user, + description=description, + instructions=instructions, + created_on=int(time.time())) + + # form validation checks for duplicates + CoprsLogic.new(user, copr, + check_for_duplicates=check_for_duplicates) + CoprChrootsLogic.new_from_names(user, copr, + selected_chroots) + return copr + + @classmethod + def new(cls, user, copr, check_for_duplicates=True): + if check_for_duplicates and cls.exists_for_user(user, copr.name).all(): + raise exceptions.DuplicateException( + "Copr: '{0}' already exists".format(copr.name)) + signals.copr_created.send(cls, copr=copr) + db.session.add(copr) + + @classmethod + def update(cls, user, copr, check_for_duplicates=True): + cls.raise_if_unfinished_blocking_action( + user, copr, "Can't change this project name," + " another operation is in progress: {action}") + + users_logic.UsersLogic.raise_if_cant_update_copr( + user, copr, "Only owners and admins may update their projects.") + + existing = cls.exists_for_user(copr.owner, copr.name).first() + if existing: + if check_for_duplicates and existing.id != copr.id: + raise exceptions.DuplicateException( + "Project: '{0}' already exists".format(copr.name)) + + else: # we're renaming + # if we fire a models.Copr.query, it will use the modified copr in session + # -> workaround this by just getting the name + old_copr_name = (db.session.query(models.Copr.name) + .filter(models.Copr.id == copr.id) + .filter(models.Copr.deleted == False) + .first()[0]) + + action = models.Action(action_type=helpers.ActionTypeEnum("rename"), + object_type="copr", + object_id=copr.id, + old_value="{0}/{1}".format(copr.owner.name, + old_copr_name), + new_value="{0}/{1}".format(copr.owner.name, + copr.name), + created_on=int(time.time())) + db.session.add(action) + db.session.add(copr) + + @classmethod + def delete(cls, user, copr, check_for_duplicates=True): + cls.raise_if_cant_delete(user, copr) + # TODO: do we want to dump the information somewhere, so that we can + # search it in future? + cls.raise_if_unfinished_blocking_action( + user, copr, "Can't delete this project," + " another operation is in progress: {action}") + + action = models.Action(action_type=helpers.ActionTypeEnum("delete"), + object_type="copr", + object_id=copr.id, + old_value="{0}/{1}".format(copr.owner.name, + copr.name), + new_value="", + created_on=int(time.time())) + copr.deleted = True + + db.session.add(action) + + return copr + + @classmethod + def exists_for_user(cls, user, coprname, incl_deleted=False): + existing = (models.Copr.query + .filter(models.Copr.name == coprname) + .filter(models.Copr.owner_id == user.id)) + + if not incl_deleted: + existing = existing.filter(models.Copr.deleted == False) + + return existing + + @classmethod + def unfinished_blocking_actions_for(cls, user, copr): + blocking_actions = [helpers.ActionTypeEnum("rename"), + helpers.ActionTypeEnum("delete")] + + actions = (models.Action.query + .filter(models.Action.object_type == "copr") + .filter(models.Action.object_id == copr.id) + .filter(models.Action.result == + helpers.BackendResultEnum("waiting")) + .filter(models.Action.action_type.in_(blocking_actions))) + + return actions + + @classmethod + def raise_if_unfinished_blocking_action(cls, user, copr, message): + """ + Raise ActionInProgressException if given copr has an unfinished + action. Return None otherwise. + """ + + unfinished_actions = cls.unfinished_blocking_actions_for( + user, copr).all() + if unfinished_actions: + raise exceptions.ActionInProgressException( + message, unfinished_actions[0]) + + @classmethod + def raise_if_cant_delete(cls, user, copr): + """ + Raise InsufficientRightsException if given copr cant be deleted + by given user. Return None otherwise. + """ + + if not user.admin and user != copr.owner: + raise exceptions.InsufficientRightsException( + "Only owners may delete their projects.") + + +class CoprPermissionsLogic(object): + + @classmethod + def get(cls, user, copr, searched_user): + query = (models.CoprPermission.query + .filter(models.CoprPermission.copr == copr) + .filter(models.CoprPermission.user == searched_user)) + + return query + + @classmethod + def get_for_copr(cls, user, copr): + query = models.CoprPermission.query.filter( + models.CoprPermission.copr == copr) + + return query + + @classmethod + def new(cls, user, copr_permission): + db.session.add(copr_permission) + + @classmethod + def update_permissions(cls, user, copr, copr_permission, + new_builder, new_admin): + + users_logic.UsersLogic.raise_if_cant_update_copr( + user, copr, "Only owners and admins may update" + " their projects permissions.") + + (models.CoprPermission.query + .filter(models.CoprPermission.copr_id == copr.id) + .filter(models.CoprPermission.user_id == copr_permission.user_id) + .update({"copr_builder": new_builder, + "copr_admin": new_admin})) + + @classmethod + def update_permissions_by_applier(cls, user, copr, copr_permission, new_builder, new_admin): + if copr_permission: + # preserve approved permissions if set + if (not new_builder or copr_permission.copr_builder != + helpers.PermissionEnum("approved")): + + copr_permission.copr_builder = new_builder + + if (not new_admin or copr_permission.copr_admin != + helpers.PermissionEnum("approved")): + + copr_permission.copr_admin = new_admin + else: + perm = models.CoprPermission( + user=user, + copr=copr, + copr_builder=new_builder, + copr_admin=new_admin) + + cls.new(user, perm) + + @classmethod + def delete(cls, user, copr_permission): + db.session.delete(copr_permission) + + +class CoprChrootsLogic(object): + + @classmethod + def mock_chroots_from_names(cls, user, names): + db_chroots = models.MockChroot.query.all() + mock_chroots = [] + for ch in db_chroots: + if ch.name in names: + mock_chroots.append(ch) + + return mock_chroots + + @classmethod + def new(cls, user, mock_chroot): + db.session.add(mock_chroot) + + @classmethod + def new_from_names(cls, user, copr, names): + for mock_chroot in cls.mock_chroots_from_names(user, names): + db.session.add( + models.CoprChroot(copr=copr, mock_chroot=mock_chroot)) + + @classmethod + def update_buildroot_pkgs(cls, copr, chroot, buildroot_pkgs): + copr_chroot = copr.check_copr_chroot(chroot) + if copr_chroot: + copr_chroot.buildroot_pkgs = buildroot_pkgs + db.session.add(copr_chroot) + + @classmethod + def update_from_names(cls, user, copr, names): + current_chroots = copr.mock_chroots + new_chroots = cls.mock_chroots_from_names(user, names) + # add non-existing + for mock_chroot in new_chroots: + if mock_chroot not in current_chroots: + db.session.add( + models.CoprChroot(copr=copr, mock_chroot=mock_chroot)) + + # delete no more present + to_remove = [] + for mock_chroot in current_chroots: + if mock_chroot not in new_chroots: + # can't delete here, it would change current_chroots and break + # iteration + to_remove.append(mock_chroot) + + for mc in to_remove: + copr.mock_chroots.remove(mc) + + +class MockChrootsLogic(object): + + @classmethod + def get(cls, user, os_release, os_version, arch, active_only=False): + return (models.MockChroot.query + .filter(models.MockChroot.os_release == os_release, + models.MockChroot.os_version == os_version, + models.MockChroot.arch == arch)) + + @classmethod + def get_from_name(cls, chroot_name, active_only=False): + """ + Return MockChroot object for textual representation of chroot + """ + + name_tuple = cls.tuple_from_name(None, chroot_name) + return cls.get(None, name_tuple[0], name_tuple[1], + name_tuple[2], active_only=active_only) + + @classmethod + def get_multiple(cls, user, active_only=False): + query = models.MockChroot.query + if active_only: + query = query.filter(models.MockChroot.is_active == True) + return query + + @classmethod + def add(cls, user, name): + name_tuple = cls.tuple_from_name(user, name) + if cls.get(user, *name_tuple).first(): + raise exceptions.DuplicateException( + "Mock chroot with this name already exists.") + new_chroot = models.MockChroot(os_release=name_tuple[0], + os_version=name_tuple[1], + arch=name_tuple[2]) + cls.new(user, new_chroot) + return new_chroot + + @classmethod + def new(cls, user, mock_chroot): + db.session.add(mock_chroot) + + @classmethod + def edit_by_name(cls, user, name, is_active): + name_tuple = cls.tuple_from_name(user, name) + mock_chroot = cls.get(user, *name_tuple).first() + if not mock_chroot: + raise exceptions.NotFoundException( + "Mock chroot with this name doesn't exist.") + + mock_chroot.is_active = is_active + cls.update(user, mock_chroot) + return mock_chroot + + @classmethod + def update(cls, user, mock_chroot): + db.session.add(mock_chroot) + + @classmethod + def delete_by_name(cls, user, name): + name_tuple = cls.tuple_from_name(user, name) + mock_chroot = cls.get(user, *name_tuple).first() + if not mock_chroot: + raise exceptions.NotFoundException( + "Mock chroot with this name doesn't exist.") + + cls.delete(user, mock_chroot) + + @classmethod + def delete(cls, user, mock_chroot): + db.session.delete(mock_chroot) + + @classmethod + def tuple_from_name(cls, user, name): + """ + valid name can be "fedora-rawhide-x86_64" or even "fedora-rawhide" + """ + + split_name = name.rsplit('-', 1) + if len(split_name) < 2: + raise exceptions.MalformedArgumentException( + "Chroot Name doesn't contain dash," + " can't determine chroot architecure.") + + if '-' in split_name[0]: + os_release, os_version = (split_name[0].rsplit('-'))[0:2] + else: + os_release, os_version = split_name[0], '' + + arch = split_name[1] + return (os_release, os_version, arch) diff --git a/frontend/coprs_frontend/coprs/logic/users_logic.py b/frontend/coprs_frontend/coprs/logic/users_logic.py new file mode 100644 index 0000000..4c2638b --- /dev/null +++ b/frontend/coprs_frontend/coprs/logic/users_logic.py @@ -0,0 +1,26 @@ +from coprs import exceptions + + +class UsersLogic(object): + + @classmethod + def raise_if_cant_update_copr(cls, user, copr, message): + """ + Raise InsufficientRightsException if given user cant update + given copr. Return None otherwise. + """ + + # TODO: this is a bit inconsistent - shouldn't the user method be + # called can_update? + if not user.can_edit(copr): + raise exceptions.InsufficientRightsException(message) + + @classmethod + def raise_if_cant_build_in_copr(cls, user, copr, message): + """ + Raises InsufficientRightsException if given user cant build in + given copr. Return None otherwise. + """ + + if not user.can_build_in(copr): + raise exceptions.InsufficientRightsException(message) diff --git a/frontend/coprs_frontend/coprs/models.py b/frontend/coprs_frontend/coprs/models.py new file mode 100644 index 0000000..b4ac6ed --- /dev/null +++ b/frontend/coprs_frontend/coprs/models.py @@ -0,0 +1,457 @@ +import datetime + +from sqlalchemy.ext.associationproxy import association_proxy +from libravatar import libravatar_url + +from coprs import constants +from coprs import db +from coprs import helpers + + +class User(db.Model, helpers.Serializer): + + """ + Represents user of the copr frontend + """ + + id = db.Column(db.Integer, primary_key=True) + # openid_name for fas, e.g. http://bkabrda.id.fedoraproject.org/ + openid_name = db.Column(db.String(100), nullable=False) + # just mail :) + mail = db.Column(db.String(150), nullable=False) + # just timezone ;) + timezone = db.Column(db.String(50), nullable=True) + # is this user proven? proven users can modify builder memory and + # timeout for single builds + proven = db.Column(db.Boolean, default=False) + # is this user admin of the system? + admin = db.Column(db.Boolean, default=False) + # stuff for the cli interface + api_login = db.Column(db.String(40), nullable=False, default="abc") + api_token = db.Column(db.String(40), nullable=False, default="abc") + api_token_expiration = db.Column( + db.Date, nullable=False, default=datetime.date(2000, 1, 1)) + + @property + def name(self): + """ + Return the short username of the user, e.g. bkabrda + """ + + return self.openid_name.replace( + ".id.fedoraproject.org/", "").replace("http://", "") + + def permissions_for_copr(self, copr): + """ + Get permissions of this user for the given copr. + Caches the permission during one request, + so use this if you access them multiple times + """ + + if not hasattr(self, "_permissions_for_copr"): + self._permissions_for_copr = {} + if not copr.name in self._permissions_for_copr: + self._permissions_for_copr[copr.name] = (CoprPermission.query + .filter_by(user=self).filter_by(copr=copr).first()) + return self._permissions_for_copr[copr.name] + + def can_build_in(self, copr): + """ + Determine if this user can build in the given copr. + """ + + can_build = False + if copr.owner == self: + can_build = True + if (self.permissions_for_copr(copr) and + self.permissions_for_copr(copr).copr_builder == + helpers.PermissionEnum("approved")): + + can_build = True + + return can_build + + def can_edit(self, copr): + """ + Determine if this user can edit the given copr. + """ + + can_edit = False + if copr.owner == self: + can_edit = True + if (self.permissions_for_copr(copr) and + self.permissions_for_copr(copr).copr_admin == + helpers.PermissionEnum("approved")): + + can_edit = True + + return can_edit + + @classmethod + def openidize_name(cls, name): + """ + Create proper openid_name from short name. + + >>> user.openid_name == User.openidize_name(user.name) + True + """ + + return "http://{0}.id.fedoraproject.org/".format(name) + + @property + def serializable_attributes(self): + # enumerate here to prevent exposing credentials + return ["id", "name"] + + @property + def coprs_count(self): + """ + Get number of coprs for this user. + """ + + return (Copr.query.filter_by(owner=self). + filter_by(deleted=False). + count()) + + @property + def gravatar_url(self): + """ + Return url to libravatar image. + """ + + try: + return libravatar_url(email=self.mail) + except IOError: + return "" + + +class Copr(db.Model, helpers.Serializer): + + """ + Represents a single copr (private repo with builds, mock chroots, etc.). + """ + + id = db.Column(db.Integer, primary_key=True) + # name of the copr, no fancy chars (checked by forms) + name = db.Column(db.String(100), nullable=False) + # string containing urls of additional repos (separated by space) + # that this copr will pull dependencies from + repos = db.Column(db.Text) + # time of creation as returned by int(time.time()) + created_on = db.Column(db.Integer) + # description and instructions given by copr owner + description = db.Column(db.Text) + instructions = db.Column(db.Text) + deleted = db.Column(db.Boolean, default=False) + + # relations + owner_id = db.Column(db.Integer, db.ForeignKey("user.id")) + owner = db.relationship("User", backref=db.backref("coprs")) + mock_chroots = association_proxy("copr_chroots", "mock_chroot") + + __mapper_args__ = { + "order_by": created_on.desc() + } + + @property + def repos_list(self): + """ + Return repos of this copr as a list of strings + """ + return self.repos.split() + + @property + def active_chroots(self): + """ + Return list of active mock_chroots of this copr + """ + + return filter(lambda x: x.is_active, self.mock_chroots) + + @property + def build_count(self): + """ + Return number of builds in this copr + """ + + return len(self.builds) + + def check_copr_chroot(self, chroot): + """ + Return object of chroot, if is related to our copr or None + """ + + result = None + # there will be max ~10 chroots per build, iteration will be probably + # faster than sql query + for copr_chroot in self.copr_chroots: + if copr_chroot.mock_chroot_id == chroot.id: + result = copr_chroot + break + return result + + def buildroot_pkgs(self, chroot): + """ + Return packages in minimal buildroot for given chroot. + """ + + result = "" + # this is ugly as user can remove chroot after he submit build, but + # lets call this feature + copr_chroot = self.check_copr_chroot(chroot) + if copr_chroot: + result = copr_chroot.buildroot_pkgs + return result + + +class CoprPermission(db.Model, helpers.Serializer): + + """ + Association class for Copr<->Permission relation + """ + + # see helpers.PermissionEnum for possible values of the fields below + # can this user build in the copr? + copr_builder = db.Column(db.SmallInteger, default=0) + # can this user serve as an admin? (-> edit and approve permissions) + copr_admin = db.Column(db.SmallInteger, default=0) + + # relations + user_id = db.Column(db.Integer, db.ForeignKey("user.id"), primary_key=True) + user = db.relationship("User", backref=db.backref("copr_permissions")) + copr_id = db.Column(db.Integer, db.ForeignKey("copr.id"), primary_key=True) + copr = db.relationship("Copr", backref=db.backref("copr_permissions")) + + +class Build(db.Model, helpers.Serializer): + + """ + Representation of one build in one copr + """ + + id = db.Column(db.Integer, primary_key=True) + # list of space separated urls of packages to build + pkgs = db.Column(db.Text) + # was this build canceled by user? + canceled = db.Column(db.Boolean, default=False) + # list of space separated additional repos + repos = db.Column(db.Text) + # the three below represent time of important events for this build + # as returned by int(time.time()) + submitted_on = db.Column(db.Integer, nullable=False) + started_on = db.Column(db.Integer) + ended_on = db.Column(db.Integer) + # url of the build results + results = db.Column(db.Text) + # memory requirements for backend builder + memory_reqs = db.Column(db.Integer, default=constants.DEFAULT_BUILD_MEMORY) + # maximum allowed time of build, build will fail if exceeded + timeout = db.Column(db.Integer, default=constants.DEFAULT_BUILD_TIMEOUT) + + # relations + user_id = db.Column(db.Integer, db.ForeignKey("user.id")) + user = db.relationship("User", backref=db.backref("builds")) + copr_id = db.Column(db.Integer, db.ForeignKey("copr.id")) + copr = db.relationship("Copr", backref=db.backref("builds")) + + chroots = association_proxy("build_chroots", "mock_chroot") + + @property + def chroot_states(self): + return map(lambda chroot: chroot.status, self.build_chroots) + + @property + def has_pending_chroot(self): + return helpers.StatusEnum("pending") in self.chroot_states + + @property + def status(self): + """ + Return build status according to build status of its chroots + """ + + if self.canceled: + return helpers.StatusEnum("canceled") + + for state in ["failed", "running", "pending", "succeeded"]: + if helpers.StatusEnum(state) in self.chroot_states: + return helpers.StatusEnum(state) + + @property + def state(self): + """ + Return text representation of status of this build + """ + + if self.status is not None: + return helpers.StatusEnum(self.status) + + return "unknown" + + @property + def cancelable(self): + """ + Find out if this build is cancelable. + + ATM, build is cancelable only if it wasn"t grabbed by backend. + """ + + return self.status == helpers.StatusEnum("pending") + + +class MockChroot(db.Model, helpers.Serializer): + + """ + Representation of mock chroot + """ + + id = db.Column(db.Integer, primary_key=True) + # fedora/epel/..., mandatory + os_release = db.Column(db.String(50), nullable=False) + # 18/rawhide/..., optional (mock chroot doesn"t need to have this) + os_version = db.Column(db.String(50), nullable=False) + # x86_64/i686/..., mandatory + arch = db.Column(db.String(50), nullable=False) + is_active = db.Column(db.Boolean, default=True) + + @property + def name(self): + """ + Textual representation of name of this chroot + """ + + if self.os_version: + format_string = "{rel}-{ver}-{arch}" + else: + format_string = "{rel}-{arch}" + return format_string.format(rel=self.os_release, + ver=self.os_version, + arch=self.arch) + + +class CoprChroot(db.Model, helpers.Serializer): + + """ + Representation of Copr<->MockChroot relation + """ + + buildroot_pkgs = db.Column(db.Text) + mock_chroot_id = db.Column( + db.Integer, db.ForeignKey("mock_chroot.id"), primary_key=True) + mock_chroot = db.relationship( + "MockChroot", backref=db.backref("copr_chroots")) + copr_id = db.Column(db.Integer, db.ForeignKey("copr.id"), primary_key=True) + copr = db.relationship("Copr", + backref=db.backref( + "copr_chroots", + single_parent=True, + cascade="all,delete,delete-orphan")) + + +class BuildChroot(db.Model, helpers.Serializer): + + """ + Representation of Build<->MockChroot relation + """ + + mock_chroot_id = db.Column(db.Integer, db.ForeignKey("mock_chroot.id"), + primary_key=True) + mock_chroot = db.relationship("MockChroot", backref=db.backref("builds")) + build_id = db.Column(db.Integer, db.ForeignKey("build.id"), + primary_key=True) + build = db.relationship("Build", backref=db.backref("build_chroots")) + status = db.Column(db.Integer, default=helpers.StatusEnum("pending")) + + @property + def name(self): + """ + Textual representation of name of this chroot + """ + + return self.mock_chroot.name + + @property + def state(self): + """ + Return text representation of status of this build chroot + """ + + if self.status is not None: + return helpers.StatusEnum(self.status) + + return "unknown" + + +class LegalFlag(db.Model, helpers.Serializer): + id = db.Column(db.Integer, primary_key=True) + # message from user who raised the flag (what he thinks is wrong) + raise_message = db.Column(db.Text) + # time of raising the flag as returned by int(time.time()) + raised_on = db.Column(db.Integer) + # time of resolving the flag by admin as returned by int(time.time()) + resolved_on = db.Column(db.Integer) + + # relations + copr_id = db.Column(db.Integer, db.ForeignKey("copr.id"), nullable=True) + # cascade="all" means that we want to keep these even if copr is deleted + copr = db.relationship( + "Copr", backref=db.backref("legal_flags", cascade="all")) + # user who reported the problem + reporter_id = db.Column(db.Integer, db.ForeignKey("user.id")) + reporter = db.relationship("User", + backref=db.backref("legal_flags_raised"), + foreign_keys=[reporter_id], + primaryjoin="LegalFlag.reporter_id==User.id") + # admin who resolved the problem + resolver_id = db.Column( + db.Integer, db.ForeignKey("user.id"), nullable=True) + resolver = db.relationship("User", + backref=db.backref("legal_flags_resolved"), + foreign_keys=[resolver_id], + primaryjoin="LegalFlag.resolver_id==User.id") + + +class Action(db.Model, helpers.Serializer): + + """ + Representation of a custom action that needs + backends cooperation/admin attention/... + """ + + id = db.Column(db.Integer, primary_key=True) + # delete, rename, ...; see helpers.ActionTypeEnum + action_type = db.Column(db.Integer, nullable=False) + # copr, ...; downcase name of class of modified object + object_type = db.Column(db.String(20)) + # id of the modified object + object_id = db.Column(db.Integer) + # old and new values of the changed property + old_value = db.Column(db.String(255)) + new_value = db.Column(db.String(255)) + # additional data + data = db.Column(db.Text) + # result of the action, see helpers.BackendResultEnum + result = db.Column( + db.Integer, default=helpers.BackendResultEnum("waiting")) + # optional message from the backend/whatever + message = db.Column(db.Text) + # time created as returned by int(time.time()) + created_on = db.Column(db.Integer) + # time ended as returned by int(time.time()) + ended_on = db.Column(db.Integer) + + def __str__(self): + return self.__unicode__() + + def __unicode__(self): + if self.action_type == helpers.ActionTypeEnum("delete"): + return "Deleting {0} {1}".format(self.object_type, self.old_value) + elif self.action_type == helpers.ActionTypeEnum("rename"): + return "Renaming {0} from {1} to {2}.".format(self.object_type, + self.old_value, + self.new_value) + elif self.action_type == helpers.ActionTypeEnum("legal-flag"): + return "Legal flag on copr {0}.".format(self.old_value) + + return "Action {0} on {1}, old value: {2}, new value: {3}.".format( + self.action_type, self.object_type, self.old_value, self.new_value) diff --git a/frontend/coprs_frontend/coprs/signals.py b/frontend/coprs_frontend/coprs/signals.py new file mode 100644 index 0000000..fde098a --- /dev/null +++ b/frontend/coprs_frontend/coprs/signals.py @@ -0,0 +1,6 @@ +import blinker + +coprs_signals = blinker.Namespace() + +build_finished = coprs_signals.signal("build-finished") +copr_created = coprs_signals.signal("copr-created") diff --git a/frontend/coprs_frontend/coprs/static/README b/frontend/coprs_frontend/coprs/static/README new file mode 100644 index 0000000..7ba0cef --- /dev/null +++ b/frontend/coprs_frontend/coprs/static/README @@ -0,0 +1,12 @@ +Public, static content goes here. Users can create rewrite rules to link to +content in the static dir. For example, django commonly uses /media/ +directories for static content. For example in a .htaccess file in a +wsgi/.htaccess location, developers could put: + +RewriteEngine On +RewriteRule ^application/media/(.+)$ /static/media/$1 [L] + +Then copy the media/* content to yourapp/wsgi/static/media/ and it should +just work. + +Note: The ^application/ part of the URI match is required. diff --git a/frontend/coprs_frontend/coprs/static/copr.css b/frontend/coprs_frontend/coprs/static/copr.css new file mode 100644 index 0000000..cb6621c --- /dev/null +++ b/frontend/coprs_frontend/coprs/static/copr.css @@ -0,0 +1,406 @@ +html, body { + font-family: Cantarell, "Droid Sans", Verdana, sans-serif; + font-size: 1em; + + color: #000; + margin: 0px; + padding: 0px; +} + +a { + color: #3d69a8; + text-decoration: none; +} + +h1 { + font-weight: normal; + color: #3d69a8; +} + +h2 { + font-size: 1.1em; +} + +#logo { + position: relative; + top: 8px; +} + +div.menu { + font-size: 0.9em; + background-image: url("header_background.png"); + background-repeat: repeat-x; + height: 81px; + margin-bottom: 3em; +} + +div.flash { + background-color: #f9f9f9; + font-size: 1.2em; + text-align: center; + padding: 0.5em; + margin-bottom: 1em; + + border-radius: 5px; + -moz-border-radius: 5px; + -webkit-border-radius: 5px; +} + +div.pagination { + width: 100%; + text-align: center; + font-size: 1.3em; + margin: 1em; +} + +div.login, div.login a { + color: white; + font-weight: bold; + text-decoration: none; + line-height: 250%; + text-align: right; + + position: relative; + margin-left: 0.3em; +} + +div.login { + float: right; +} + +div.login .text { + font-weight: normal; +} + +div.page, div.menu-inner { + width: 780px; + margin-left: auto; + margin-right: auto; +} + +div.user-info { + width: 174px; + margin-right: 25px; + float: left; + border-right: 1px solid #c3c3c3; + + color: #565656; + font-size: 1.5em; + font-weight: bold; +} + +div.user-info .coprs-count { + margin: 0px; + font-size: 0.8em; +} + +div.user-info .other-text { + margin: 0px; + font-size: 0.6em; +} + +div.about-copr { + width: 100%; + background-color: #ececec; + padding-top: 1em; + padding-bottom: 1em; + + border-radius: 5px; + -moz-border-radius: 5px; + -webkit-border-radius: 5px; +} + +div.about-copr p { + padding-left: 1em; + padding-right: 1em; + margin-top: 0.3em; + margin-bottom: 0.3em; +} + +div.coprs-list-thin, div.coprs-list-thick { + float: left; +} + +div.coprs-list-thin { + width: 580px; +} + +div.coprs-list-thick { + width: 100%; +} + +div.copr { + width: 100%; + background-color: #f9f9f9; + padding-top: 1em; + padding-bottom: 1em; + + border-radius: 5px; + -moz-border-radius: 5px; + -webkit-border-radius: 5px; +} + +a.coprs-list { + padding-left: 0.71em; + padding-right: 0.71em; + font-size: 1.4em; + font-weight: bold; + display: block; +} + +div.copr p { + padding-left: 1em; + padding-right: 1em; + margin-top: 0.3em; + margin-bottom: 0.3em; +} + +div.copr .repos { + color: #808080; +} + +div.search-results { + font-size: 1.3em; + text-align: center; +} + +div.add-copr { + background-color: #ececec; + padding: 0.5em; + margin-bottom: 0.6em; + + color: #cccccc; + text-align: center; + vertical-align: middle; + font-size: 1.1em; + font-weight: bold; + + border-radius: 5px; + -moz-border-radius: 5px; + -webkit-border-radius: 5px; +} + +div.add-copr a { + text-decoration: none; + color: #666666; +} + +div.horizontal-menu { + background-color: #ededed; + height: 3em; + + border-radius: 10px; + -moz-border-radius: 10px; + -webkit-border-radius: 10px; +} + +div.horizontal-menu a { + display: block; + padding: 0.6em; + font-size: 1.1em; + font-weight: bold; + color: #4d4d4d; +} + +div.horizontal-menu ul { + margin: 0px; + padding: 0px; + float: left; +} + +div.horizontal-menu li { + display: inline; + float: left; +} + +div.horizontal-menu li.selected a, div.horizontal-menu li.left-for-now a { + color: #db3279; + margin-left: auto; + margin-right: auto; +} + +div.horizontal-menu li.selected a, div.horizontal-menu li.hovered a { + background: url("pink_arrow.png") no-repeat; + background-position: center bottom; +} + +div.pkg-url-list { + white-space: pre-wrap; + background-color: #f9f9f9; + font-family: monospace; + padding: 1em; + line-height: 120%; + font-size: 1.1em; +} + +div.shift-right { + margin-left: 1em; +} + +dt.field-label { + margin: 15px 0; + font-weight: bold; +} + +input.rounded { + padding-left: 4px; + border-radius: 10px; + -moz-border-radius: 10px; + -webkit-border-radius: 10px; +} + +input.fulltext-submit { + color: white; + font-weight: bold; + background-color: #3d69a8; + border-radius: 10px; + -moz-border-radius: 10px; + -webkit-border-radius: 10px; +} + +p.form-error { + color: red; +} + +table.releases { + width: 100%; + border-collapse:collapse; +} + +table.chroots-set { + display: inline; + margin-left: 40px; +} + +table.builds-table { + width: 100%; +} + +table.builds-table form { + display: inline; +} + +table.builds-table tr.details { + width: 100%; + display: none; +} + +tr.build-state:hover { + text-decoration: underline; + background-color: #E6E6E6; +} + +.build-pending { + color: #3B6EB4; +} + +.build-running { + color: #FF6600; +} + +.build-succeeded { + color: #22DD22; +} + +.build-failed { + color: #DD2222; +} + +.build-canceled { + color: #CDC90C; +} + +table.releases th { + background-color: #f2f2f2; + text-align: left; +} + +table.releases th.leftmost { + border-top-left-radius: 10px; + -moz-border-radius-topleft: 10px; + -webkit-border-top-left-radius: 10px; + border-bottom-left-radius: 10px; + -moz-border-radius-bottomleft: 10px; + -webkit-border-bottom-left-radius: 10px; +} + +table.releases th.rightmost { + border-top-right-radius: 10px; + -moz-border-radius-topright: 10px; + -webkit-border-top-right-radius: 10px; + border-bottom-right-radius: 10px; + -moz-border-radius-bottomright: 10px; + -webkit-border-bottom-right-radius: 10px; +} + +table.releases tr.release-end { + border-bottom: 3px solid #f2f2f2; +} + +table.monitor { + margin: 0 auto; + width: 90%; +} + +form.legal-flag, form.legal-flag input { + color: #888888; + margin-top: 5px; +} + +div.legal-flag { + margin: 10px; + font-size: 1.2em; +} + +div.legal-flag div.message { + display: none; + margin-bottom: 10px; + font-size: 1em; +} + +div.legal-flag form { + display: inline; + text-align: right; + float: right; +} + +hr { + margin-top: 25px; + margin-bottom: 25px; +} + + +.footer { + padding: 25px; + background: white; + font-size: 0.8em; +} + +.footer p { + margin: 0 auto; + width: 300px; +} + +.footer a { + margin-right: 10px; + padding-right: 10px; + border-right: 1px solid #444; +} + +.footer .last { + border-right: none; +} + +.required:before { + content: "* "; + color: red; + font-weight: bold; +} + +textarea { + width: 100%; +} + +.centered { + text-align: center; +} diff --git a/frontend/coprs_frontend/coprs/static/copr.js b/frontend/coprs_frontend/coprs/static/copr.js new file mode 100644 index 0000000..41b9fd6 --- /dev/null +++ b/frontend/coprs_frontend/coprs/static/copr.js @@ -0,0 +1,29 @@ +// showing build details +$(document).ready(function () { + $("table.builds-table tr[class^='build-']").each(function (i, e) { + $(this).click(function() { $("table.builds-table tr.details").hide(); $(this).next().show(); }); + }); +}); + +// build detail menu arrow slider +$(document).ready(function() { + $("div.horizontal-menu li").click( + function() { + $("div.horizontal-menu li.selected").removeClass('selected').addClass('left-for-now'); + $(this).toggleClass('clicked'); + }, + function() { + $("div.horizontal-menu li.left-for-now").removeClass('left-for-now').addClass('selected'); + $(this).toggleClass('clicked'); + } + ); +}); + +// admin legal-flag divs rolling +$(document).ready(function() { + $("div.legal-flag").mouseenter( + function() { + $(this).children(".message").show("fast"); + } + ); +}); diff --git a/frontend/coprs_frontend/coprs/static/copr_logo.png b/frontend/coprs_frontend/coprs/static/copr_logo.png new file mode 100644 index 0000000..4576f78 Binary files /dev/null and b/frontend/coprs_frontend/coprs/static/copr_logo.png differ diff --git a/frontend/coprs_frontend/coprs/static/default_user.png b/frontend/coprs_frontend/coprs/static/default_user.png new file mode 100644 index 0000000..31b6c60 Binary files /dev/null and b/frontend/coprs_frontend/coprs/static/default_user.png differ diff --git a/frontend/coprs_frontend/coprs/static/favicon.ico b/frontend/coprs_frontend/coprs/static/favicon.ico new file mode 100644 index 0000000..79d0ba9 Binary files /dev/null and b/frontend/coprs_frontend/coprs/static/favicon.ico differ diff --git a/frontend/coprs_frontend/coprs/static/header_background.png b/frontend/coprs_frontend/coprs/static/header_background.png new file mode 100644 index 0000000..61fcb6f Binary files /dev/null and b/frontend/coprs_frontend/coprs/static/header_background.png differ diff --git a/frontend/coprs_frontend/coprs/static/pink_arrow.png b/frontend/coprs_frontend/coprs/static/pink_arrow.png new file mode 100644 index 0000000..fec3cf7 Binary files /dev/null and b/frontend/coprs_frontend/coprs/static/pink_arrow.png differ diff --git a/frontend/coprs_frontend/coprs/templates/404.html b/frontend/coprs_frontend/coprs/templates/404.html new file mode 100644 index 0000000..bfd8bfd --- /dev/null +++ b/frontend/coprs_frontend/coprs/templates/404.html @@ -0,0 +1,10 @@ +{% extends "layout.html" %} +{% block title %}Not Found{% endblock %} +{% block header %}Not Found!{% endblock %} +{% block body %} + {% if message %} + {{ message }} + {% else %} + The thing you're looking for does not exist. + {% endif %} +{% endblock %} diff --git a/frontend/coprs_frontend/coprs/templates/_helpers.html b/frontend/coprs_frontend/coprs/templates/_helpers.html new file mode 100644 index 0000000..04630bb --- /dev/null +++ b/frontend/coprs_frontend/coprs/templates/_helpers.html @@ -0,0 +1,33 @@ +{% macro render_field(field, label=None, class='') %} + {% if not kwargs['hidden'] %} +
{{ label or field.label }}
+
+ {% if field.errors %} + {% for error in field.errors %} +

{{ error }}

+ {% endfor %} + {% endif %} + {{ field(**kwargs)|safe }} +
+ {% else %} + {{ field(**kwargs)|safe }} + {% endif %} +{% endmacro %} + +{% macro render_pagination(request, paginator) %} + {% if paginator.pages > 1 %} + {% if paginator.border_url(request, True) %} + {{ paginator.border_url(request, True)[1] }} ... + {% endif %} + {% for page in paginator.get_urls(request) %} + {% if page[1] != paginator.page %} {# no url for current page #} + {{ page[1] }} + {% else %} + {{ page[1] }} + {% endif %} + {% endfor %} + {% if paginator.border_url(request, False) %} + ... {{ paginator.border_url(request, False)[1] }} + {% endif %} + {% endif %} +{% endmacro %} diff --git a/frontend/coprs_frontend/coprs/templates/admin/index.html b/frontend/coprs_frontend/coprs/templates/admin/index.html new file mode 100644 index 0000000..25562a2 --- /dev/null +++ b/frontend/coprs_frontend/coprs/templates/admin/index.html @@ -0,0 +1,5 @@ +{% extends "admin/layout.html" %} + +{% block admin_body %} +Admin body +{% endblock %} diff --git a/frontend/coprs_frontend/coprs/templates/admin/layout.html b/frontend/coprs_frontend/coprs/templates/admin/layout.html new file mode 100644 index 0000000..c8d48c9 --- /dev/null +++ b/frontend/coprs_frontend/coprs/templates/admin/layout.html @@ -0,0 +1,14 @@ +{% extends "layout.html" %} + +{% block title %}Coprs - Admin{% endblock %} + +{% block body %} +
+ +
+{% block admin_body %}{% endblock %} +{% endblock %} diff --git a/frontend/coprs_frontend/coprs/templates/admin/legal-flag.html b/frontend/coprs_frontend/coprs/templates/admin/legal-flag.html new file mode 100644 index 0000000..75f61c1 --- /dev/null +++ b/frontend/coprs_frontend/coprs/templates/admin/legal-flag.html @@ -0,0 +1,34 @@ +{% extends "admin/layout.html" %} + +{% block legal_flag_selected %}selected{% endblock %} + +{% block admin_body %} + {% for flag in legal_flags %} + + {% else %} +

No coprs marked for legal review

+ {% endfor %} +{% endblock %} diff --git a/frontend/coprs_frontend/coprs/templates/api.html b/frontend/coprs_frontend/coprs/templates/api.html new file mode 100644 index 0000000..9b511f3 --- /dev/null +++ b/frontend/coprs_frontend/coprs/templates/api.html @@ -0,0 +1,448 @@ +{% extends "layout.html" %} +{% block title %}API for Copr{% endblock %} +{% block header %}API for the Copr Build System{% endblock %} +{% block body %} + {% if error %}

Error: {{ error }}

{% endif %} + +
+

Copr API

+ +

API Token

+

In order to access the API, you will need to provide an API token. + This token is unique, specific to you and + should not be shared!. +

+ +

The API token is valid for {{ config['API_TOKEN_EXPIRATION'] }} days after it has been generated. +

+ + {% if g.user %} +

Your information (you can directly paste this into ~/.config/copr):

+
+[copr-cli]
+login = {{ g.user.api_login }}
+username = {{ g.user.name }}
+token = {{ g.user.api_token }}
+copr_url = http://copr.fedoraproject.org
+# expiration date: {{ g.user.api_token_expiration }}
+
+ + + + + {% else %} +

You need to be logged in to see your API token.

+ {% endif %} + +

The API

+ +

To make an API call to Copr, make a request to URL corresponding to + given call (URLs are listed below). Parameters are denoted by angle + brackets. Result is represented as JSON map with "output": "ok" key-value + pair on success or "output": "notok" on failure. The rest of the map + represents result of the call and is described below for individual + calls.

+ +

List someone's projects

+ +

URL:

+
/api/coprs/<username>/
+
or
+
/api/coprs/?username="<username>"
+ +

URL parameters:

+
    +
  • username – The name of the user whose projects you'd like + to list
  • +
+ +

Result:

+
    +
  • "repos" – List of projects in given format: +
      +
    • "yum_repos" – Map of chroots to yum repository + URLs. Chroots are in format + "<release>-<version>-<architecture>"
    • +
    • "additional_repos" – List of additional + repositories that are required for this project
    • +
    • "instructions" – Installation instructions + provided by project's owner
    • +
    • "name" – Name of the project
    • +
    • "description" – Description provided by project's + owner
    • +
    +
+ +

Example call URL

+
https://copr.fedoraproject.org/api/coprs/jdaniels/
+ +

Example results

+
+    {
+      "output": "ok",
+      "repos": [
+        {
+          "yum_repos": {
+            "fedora-19-i686": "https://copr-be.cloud.fedoraproject.org/results/jdaniels/log4j/fedora-19-i686/",
+            "fedora-19-x86_64": "https://copr-be.cloud.fedoraproject.org/results/jdaniels/log4j/fedora-19-x86_64/"
+          },
+          "additional_repos": "",
+          "instructions": "",
+          "name": "log4j",
+          "description": "Java logging package"
+        }
+      ]
+    }
+    
+ +

Detail of project

+ +

URL:

+
/api/coprs/<username>/<projectname>/detail/
+ +

URL parameters:

+
    +
  • username – The name of the user whose projects you'd like + to show
  • +
  • projectname – The name of the project
  • + +
+ +

Result:

+
    +
  • "detial" – dictionary with following keys: +
      +
    • "yum_repos" – Map of chroots to yum repository + URLs. Chroots are in format + "<release>-<version>-<architecture>"
    • +
    • "additional_repos" – List of additional + repositories that are required for this project
    • +
    • "instructions" – Installation instructions + provided by project's owner
    • +
    • "name" – Name of the project
    • +
    • "description" – Description provided by project's + owner
    • +
    • "last_modified" – Datetime (in epoch format) of + last successfull build.
    • +
    +
+ +

Example call URL

+
https://copr.fedoraproject.org/api/coprs/jdaniels/log4j/detail/
+ +

Example results

+
+    {
+      "output": "ok",
+      "repos": [
+        {
+          "yum_repos": {
+            "fedora-19-i686": "https://copr-be.cloud.fedoraproject.org/results/jdaniels/log4j/fedora-19-i686/",
+            "fedora-19-x86_64": "https://copr-be.cloud.fedoraproject.org/results/jdaniels/log4j/fedora-19-x86_64/"
+          },
+          "additional_repos": "",
+          "instructions": "",
+          "name": "log4j",
+          "description": "Java logging package",
+          "last_modified": 1386695673 
+        }
+      ]
+    }
+    
+ +

Create new project

+ +

Login required

+ +

URL:

+
/api/coprs/<username>/new/
+ +

URL parameters:

+
    +
  • username – The name of the user whose project should be + created
  • +
+ +

Parameters sent by POST:

+
    +
  • name – The name of the project to be created
  • +
  • chroots – Chroots to be used in the project, specified as chrootname=y + (e.g.: fedora-rawhide-x86_64=y&fedora-20-x86_64=y)
  • +
  • repos – A space separated list of repositories + that this new project should have access to
  • +
  • initial_pkgs – A space separated list of initial + packages to build in this new project
  • +
+ +

Add new build

+ +

Login required

+ +

URL:

+
/api/coprs/<username>/<projectname>/new_build/
+ +

URL parameters:

+
    +
  • username – The name of given projects's owner
  • +
  • projectname – The name of the project in which the package + should be built
  • +
+ +

Parameters sent by POST:

+
    +
  • pkgs – Space separated list of package URLs (SRPMs) to be built
  • +
+ +

Example results

+
+    {
+      "output": "ok",
+      "message": "Build was added to log4j.",
+      "id": 5
+    }
+    
+ +

Query build status

+ +

Login required

+ +

URL:

+
/api/coprs/build_status/<build_id>/
+ +

URL parameters:

+
    +
  • build_id – Build ID returned by the new_build call
  • +
+ +

Result

+
    +
  • status – Status of the build, one of +
      +
    • pending
    • +
    • running
    • +
    • failed
    • +
    • succeeded
    • +
    • canceled
    • +
    +
  • +
+ +

Example result

+
+    {
+      "status": "pending",
+      "output": "ok"
+    }
+    
+ +

Query build detail

+ +

URL:

+
/api/coprs/build_detail/<build_id>/
+ +

URL parameters:

+
    +
  • build_id – Build ID returned by the new_build call
  • +
+ +

Result

+
    +
  • status – Status of the build, one of +
      +
    • pending
    • +
    • running
    • +
    • failed
    • +
    • succeeded
    • +
    • canceled
    • +
    +
  • +
  • owner – Name of the owner. +
  • +
  • project – Name of the project. +
  • +
+ +

Example result

+
+    {
+      "status": "pending",
+      "owner": "msuchy",
+      "project": "myproject",
+      "output": "ok"
+    }
+    
+ +

Cancel build

+ +

Login required

+ +

URL:

+
/api/coprs/cancel_build/<build_id>/
+ +

URL parameters:

+
    +
  • build_id – Build ID returned by the new_build call
  • +
+ +

Result

+
    +
  • status – Status of the task. +
+ +

Example result

+
+    {
+      "status": "Build canceled",
+      "output": "ok"
+    }
+    
+ +

Copr Modification

+ +

Login required

+ +

URL:

+
/api/coprs/<username>/<coprname>/modify/
+ +

URL parameters:

+
    +
  • username – Name of the user whose copr you'd like to modify
  • +
  • coprname – Name of the copr you'd you'd like to modify
  • +
+ +

Parameters sent by POST:

+
    +
  • description – Brief description of the project (Optional)
  • +
  • instructions – Description of how your project can be + installed, etc. (Optional)
  • +
  • repos – URL to additional yum repos, which can be used + during build. Space separated. (Optional)
  • +
+ +

Result

+
    +
  • description – Same as above
  • +
  • instructions – Same as above
  • +
  • repos – Same as above
  • +
+ +

Example result

+
+    {
+      "output": "ok",
+      "repos": "foo",
+      "description": "bar",
+      "instructions": "baz"
+    }
+    
+ +

Chroot Modification

+ +

Login required

+ +

URL:

+
/api/coprs/<username>/<coprname>/modify/<chrootname>/
+ +

URL parameters:

+
    +
  • username – Name of the user whose chroot you'd like to modify
  • +
  • coprname – Name of the copr whose chroot you'd like to modify
  • +
  • chrootname – Name of the chroot you'd like to modify
  • +
+ +

Parameters sent by POST:

+
    +
  • buildroot_pkgs – Additional packages to be always present in minimal + buildroot
  • +
+ +

Result

+
    +
  • buildroot_pkgs – Same as above
  • +
+ +

Example result

+
+    {
+      "output": "ok",
+      "buildroot_pkgs": "scl-utils-build"
+    }
+    
+ +

Chroot details

+ +

URL:

+
/api/coprs/<username>/<coprname>/detail/<chrootname>/
+ +

URL parameters:

+
    +
  • username – Name of the user whose chroot you'd like to know details of
  • +
  • coprname – Name of the copr whose chroot you'd like to know details of
  • +
  • chrootname – Name of the chroot you'd like to know details of
  • +
+ +

Result

+
    +
  • buildroot_pkgs – Additional packages to be always present in minimal + buildroot
  • +
+ +

Example result

+
+    {
+      "output": "ok",
+      "buildroot_pkgs": "scl-utils-build"
+    }
+    
+ +

Search for project

+ +

URL:

+
/api/coprs/search/<project>/
+
or
+
/api/coprs/?search="<project>"
+ +

URL parameters:

+
    +
  • project – The text of the project whose you'd like + to find
  • +
+ +

Result:

+
    +
  • "repos" – List of repos in given format: +
      +
    • "username" – Name of the user
    • +
    • "coprname" – Name of the copr
    • +
    • "description" – Description of the copr
    • +
    +
+ +

Example call URL

+
https://copr.fedoraproject.org/api/coprs/search/tests/
+ +

Example results

+
+    {
+      "output": "ok",
+      "repos": [
+        {
+          "username": "ignatenkobrain",
+          "coprname": "test",
+          "description": "Tests"
+        },
+          "username": "ignatenkobrain",
+          "coprname": "tests",
+          "description": ""
+        },
+        {
+          "username": "msuchy",
+          "coprname": "tests",
+          "description": "Copr testing repository, just for test various builds."
+        }
+      ]
+    }
+    
+ +
+{% endblock %} diff --git a/frontend/coprs_frontend/coprs/templates/coprs/_coprs_forms.html b/frontend/coprs_frontend/coprs/templates/coprs/_coprs_forms.html new file mode 100644 index 0000000..6abe22f --- /dev/null +++ b/frontend/coprs_frontend/coprs/templates/coprs/_coprs_forms.html @@ -0,0 +1,114 @@ +{% from "_helpers.html" import render_field %} + +{% macro copr_form(form, view, copr = None, username = None) %} + {# if using for updating, we need to pass name to url_for, but otherwise we need to pass nothing #} +
+
+ {{ form.csrf_token }} + {{ render_field(form.id, hidden = True) }} + {% if copr is none %} + {{ render_field(form.name, label='Project Name', required = True, class="required") }} + {% else %} + {{ render_field(form.name, hidden = True) }} + {{ render_field(form.name, label='Project Name', disabled = True) }} + {% endif %} + {{ render_field(form.description, rows=5, cols=50, placeholder='Optional - describe your project briefly.') }} + {{ render_field(form.instructions, rows=5, cols=50, placeholder='Optional - describe how your project can be installed. Link to wiki is good as well.') }} +
You can use markdown syntax, inline HTML is forbidden..
+
Chroots
+ {% if form._mock_chroots_error %} +

{{ form._mock_chroots_error }}

+ {% endif %} + {% for group_set, chs in form.chroots_sets.items() %} + + {% for ch in chs %} + + + + {% endfor %} +
+ {{ form|attr(ch)|attr('label') }} + {% if form|attr(ch)|attr('label') %} + {% else %} + {{ form|attr(ch)|attr('label') }} + {% endif %} + {{ form|attr(ch) }} + {% if copr and form|attr(ch)|attr('data') %} + [Edit] + {% endif %} +
+ {% endfor %} + {{ render_field(form.repos, rows=5, cols=50, placeholder='Optional - URL to additional yum repos, which can be used during build. Space separated.') }} + {% if copr is none %}{# we're creating the copr, so display initial builds area #} + {{ render_field(form.initial_pkgs, rows=5, cols=50, placeholder='Optional - list of src.rpm to build initially. Can be skipped and submitted later.') }} + {% endif %} +
+
+
+{% endmacro %} + +{% macro copr_delete_form(form, copr) %} +
+
+ {{ form.csrf_token }} +
+ {% if form.verify.errors %} + {% for error in form.verify.errors %} +

{{ error }}

+ {% endfor %} + {% endif %} +
+ {{ form.verify }} +
+
+
+{% endmacro %} + +{% macro copr_permissions_form(form, copr, permissions) %} + {% if permissions %} +
+ {{ form.csrf_token }} + + + {% for perm in permissions %} + + + + + + {% endfor %} +
UsernameIs BuilderIs Admin
{{ perm.user.name }} + {{ perm.copr_builder|perm_type_from_num }} + {% if perm.copr_builder != 0 %} + {{ form['copr_builder_{0}'.format(perm.user.id)] }} + {% endif %} + + {{ perm.copr_admin|perm_type_from_num }} + {% if perm.copr_admin != 0 %} + {{ form['copr_admin_{0}'.format(perm.user.id)] }} + {% endif %} +
+
+
+ {% endif %} + {% endmacro %} + +{% macro copr_legal_flag_form(form, copr) %} + +{% endmacro %} diff --git a/frontend/coprs_frontend/coprs/templates/coprs/add.html b/frontend/coprs_frontend/coprs/templates/coprs/add.html new file mode 100644 index 0000000..21d901e --- /dev/null +++ b/frontend/coprs_frontend/coprs/templates/coprs/add.html @@ -0,0 +1,15 @@ +{% extends "layout.html" %} +{% block title %}Add a Project{% endblock %} +{% block header %}Add a new Project{% endblock %} +{% from "coprs/_coprs_forms.html" import copr_form %} + +{% block body %} + +

Add a new Project

+ +

+You agree to build only allowed content in Copr. Check if your license is allowed. +

+ {{ copr_form(form, view = 'coprs_ns.copr_new', username=g.user.name) }} + +{% endblock %} diff --git a/frontend/coprs_frontend/coprs/templates/coprs/copr.repo b/frontend/coprs_frontend/coprs/templates/coprs/copr.repo new file mode 100644 index 0000000..1aaf318 --- /dev/null +++ b/frontend/coprs_frontend/coprs/templates/coprs/copr.repo @@ -0,0 +1,6 @@ +[{{ copr.owner.name }}-{{ copr.name }}] +name=Copr repo for {{ copr.name }} owned by {{ copr.owner.name }} +baseurl={{ url }} +skip_if_unavailable=True +gpgcheck=0 +enabled=1 diff --git a/frontend/coprs_frontend/coprs/templates/coprs/detail.html b/frontend/coprs_frontend/coprs/templates/coprs/detail.html new file mode 100644 index 0000000..cc4535c --- /dev/null +++ b/frontend/coprs_frontend/coprs/templates/coprs/detail.html @@ -0,0 +1,41 @@ +{% extends "layout.html" %} +{% block title %}{{ copr.owner.name }}/{{ copr.name }} Copr{% endblock %} + +{% block body %} +

+ {{ copr.owner.name }} / + {{ copr.name }} +

+
+
    +
  • + Overview +
  • +
  • + Permissions +
  • +
  • + Builds +
  • +
  • + Monitor +
  • + {% if g.user and g.user.can_build_in(copr) %} +
  • + New Build +
  • + {% endif %} + {% if g.user and g.user.can_edit(copr) %} +
  • + Edit +
  • + {% endif %} + {% if g.user and g.user == copr.owner %} +
  • + Delete +
  • + {% endif %} +
+
+ {% block detail_body %}{% endblock %} +{% endblock %} diff --git a/frontend/coprs_frontend/coprs/templates/coprs/detail/_builds_forms.html b/frontend/coprs_frontend/coprs/templates/coprs/detail/_builds_forms.html new file mode 100644 index 0000000..f6b7bd2 --- /dev/null +++ b/frontend/coprs_frontend/coprs/templates/coprs/detail/_builds_forms.html @@ -0,0 +1,51 @@ +{% from "_helpers.html" import render_field %} + +{% macro copr_build_form(form, view, copr) %} +
+
+ {{ form.csrf_token }} + {{ render_field(form.pkgs, label='URLs of packages to build', rows = 10, cols = 50) }} + {% if g.user.proven %} + {{ render_field(form.memory_reqs) }} + {{ render_field(form.timeout) }} + {% else %} {# once we pass the hidden attribute, the field will just be hidden, it seems #} + {{ render_field(form.memory_reqs, hidden = True) }} + {{ render_field(form.timeout, hidden = True) }} + {% endif %} +
+

+ You agree to build only allowed content in Copr. + Check if your license is allowed. +

+
+
+
+
+{% endmacro %} + +{% macro copr_build_cancel_form(build, page) %} + {% if build.cancelable %} +
+ + +
+ {% endif %} +{% endmacro %} + +{% macro copr_build_repeat_form(build, page) %} + {% if build.cancelable %} +
+ + +
+ {% endif %} +{% endmacro %} + +{% macro copr_build_delete_form(build, page) %} + {% if build.ended_on %} +
+ + +
+ {% endif %} +{% endmacro %} diff --git a/frontend/coprs_frontend/coprs/templates/coprs/detail/_builds_table.html b/frontend/coprs_frontend/coprs/templates/coprs/detail/_builds_table.html new file mode 100644 index 0000000..720ef76 --- /dev/null +++ b/frontend/coprs_frontend/coprs/templates/coprs/detail/_builds_table.html @@ -0,0 +1,62 @@ +{% from "coprs/detail/_builds_forms.html" import copr_build_cancel_form, copr_build_repeat_form, copr_build_delete_form %} + +{% macro builds_table(builds, page) %} + {% if builds %} + + + + + + + + + + {% for build in builds %} + + +{% if g.user %} + +{% else %} + +{% endif %} + + +{% if g.user %} + + +{% else %} + + +{% endif %} + + + + + + {% endfor %} +
IdSubmitted onSubmitted byStarted onEnded onState
{{ build.id }}{{ build.submitted_on|localized_time(g.user.timezone) }}{{ build.submitted_on|localized_time("UTC") }}{{ build.user.name }}{{ build.started_on|localized_time(g.user.timezone) }}{{ build.ended_on|localized_time(g.user.timezone) }}{{ build.started_on|localized_time("UTC") }}{{ build.ended_on|localized_time("UTC") }}{{ build.state }}
+
+ {% if g.user and g.user.can_build_in(copr) %} + {{ copr_build_cancel_form(build, page) }} + {% endif %} + {% if g.user and g.user.can_build_in(copr) %} + {{ copr_build_repeat_form(build, page) }} + {% endif %} + {% if g.user and g.user.can_edit(copr) %} + {{ copr_build_delete_form(build, page) }} + {% endif %} +
+ {% if build.results %} +

Results:

{{ build.results }} + {% else %} +

No results yet.

+ {% endif %} +
+

Package URLs:

+
{% if build.pkgs is not none %}{% for pkg in build.pkgs.split() %}{{ pkg }} +{% endfor %}{% endif %}
+
+ {% else %} +

No builds so far

+ {% endif %} +{% endmacro %} diff --git a/frontend/coprs_frontend/coprs/templates/coprs/detail/_permissions_table.html b/frontend/coprs_frontend/coprs/templates/coprs/detail/_permissions_table.html new file mode 100644 index 0000000..d8ded5d --- /dev/null +++ b/frontend/coprs_frontend/coprs/templates/coprs/detail/_permissions_table.html @@ -0,0 +1,90 @@ +{% macro permissions_table(permissions, current_user_permissions, copr, permissions_applier_form, permissions_form) %} + {% if permissions or g.user != copr.owner %} {# display the whole table if there are permissions or user can ask for them #} + {% if permissions_applier_form and g.user %} +
+ {{ permissions_applier_form.csrf_token }} + {% endif %} + {% if permissions_form and g.user %} + + {{ permissions_form.csrf_token }} + {% endif %} + + + {% for perm in permissions %} + {% if perm.user_id != g.user.id %} {# if user is logged in, only display his form below, not a row #} + {{ permissions_table_row_other_user(perm, permissions_applier_form, permissions_form) }} + {% endif %} + {% endfor %} + {{ permissions_table_row_current_user(current_user_permissions, permissions_applier_form, permissions_form) }} +
UsernameIs BuilderIs Admin
+ {% if g.user and (permissions_applier_form or permissions_form) %} {# TODO: when to display? #} + +
+ {% endif %} + {% else %} + No permissions for other users for this Copr. + {% endif %} +{% endmacro %} + +{% macro permissions_table_row_other_user(perm, permissions_applier_form, permissions_form) %} + + {{ perm.user.name }} + + {% if permissions_form %} + {% if perm.copr_builder != 0 %} + {{ permissions_form['copr_builder_{0}'.format(perm.user.id)] }} + {% endif %} + {% else %} + {{ perm.copr_builder|perm_type_from_num }} + {% endif %} + + + {% if permissions_form %} + {% if perm.copr_admin != 0 %} + {{ permissions_form['copr_admin_{0}'.format(perm.user.id)] }} + {% endif %} + {% else %} + {{ perm.copr_admin|perm_type_from_num }} + {% endif %} + + +{% endmacro %} + +{% macro permissions_table_row_current_user(current_user_permissions, permissions_applier_form, permissions_form) %} + {# if user is logged in and permissions_applier_form is defined, display it #} + {% if g.user and permissions_applier_form %} + + {{ g.user.name }} + + {% if current_user_permissions %} + {{ current_user_permissions.copr_builder|perm_type_from_num }} + {% else %} + Not requested + {% endif %} +
+ {{ permissions_applier_form.copr_builder|safe }} + + + {% if current_user_permissions %} + {{ current_user_permissions.copr_admin|perm_type_from_num }} + {% else %} + Not requested + {% endif %} +
+ {{ permissions_applier_form.copr_admin|safe }} + + {% endif %} + + {# if user is admin (means current_user_permissions is set), display his own permissions for changing #} + {% if g.user and permissions_form and current_user_permissions %} + + {{ g.user.name }} + + {{ permissions_form['copr_builder_{0}'.format(g.user.id)] }} + + + {{ permissions_form['copr_admin_{0}'.format(g.user.id)] }} + + + {% endif %} +{% endmacro %} diff --git a/frontend/coprs_frontend/coprs/templates/coprs/detail/add_build.html b/frontend/coprs_frontend/coprs/templates/coprs/detail/add_build.html new file mode 100644 index 0000000..c0b1b61 --- /dev/null +++ b/frontend/coprs_frontend/coprs/templates/coprs/detail/add_build.html @@ -0,0 +1,10 @@ +{% extends "coprs/detail.html" %} +{% block title %}Adding Build for {{ copr.owner.name }}/{{ copr.name }}{% endblock %} +{% block new_build_selected %}selected{% endblock %} +{% from "coprs/detail/_builds_forms.html" import copr_build_form with context %} + +{% block detail_body %} + + {{ copr_build_form(form, 'coprs_ns.copr_new_build', copr) }} + +{% endblock %} diff --git a/frontend/coprs_frontend/coprs/templates/coprs/detail/builds.html b/frontend/coprs_frontend/coprs/templates/coprs/detail/builds.html new file mode 100644 index 0000000..3aca8bf --- /dev/null +++ b/frontend/coprs_frontend/coprs/templates/coprs/detail/builds.html @@ -0,0 +1,16 @@ +{% extends "coprs/detail.html" %} +{% block title %}Builds for {{ copr.owner.name }}/{{ copr.name }}{% endblock %} +{% block builds_selected %}selected{% endblock %} +{% from "_helpers.html" import render_pagination %} +{% from "coprs/detail/_builds_table.html" import builds_table with context %} + +{% block detail_body %} + {% if builds %} + {{ builds_table(builds, paginator.page) }} + + {% else %} +

No builds so far.

+ {% endif %} +{% endblock %} diff --git a/frontend/coprs_frontend/coprs/templates/coprs/detail/delete.html b/frontend/coprs_frontend/coprs/templates/coprs/detail/delete.html new file mode 100644 index 0000000..f0e22b9 --- /dev/null +++ b/frontend/coprs_frontend/coprs/templates/coprs/detail/delete.html @@ -0,0 +1,14 @@ +{% extends "coprs/detail.html" %} +{% block title %}Delete {{ copr.owner.name }}/{{ copr.name }}?{% endblock %} +{% block delete_selected %}selected{% endblock %} +{% from "coprs/_coprs_forms.html" import copr_delete_form %} + +{% block detail_body %} +

If you really want to delete this Project, you'll have to answer this riddle:

+

{{ range(5)|random }}.{{ range(10)|random }} hens lay {{ range(5)|random }}.{{ range(10)|random }} eggs in + {{ range(5)|random }}.{{ range(10)|random }} days. How many eggs do {{ range(5)|random }}.{{ range(10)|random }} + hens lay in {{ range(5)|random }}.{{ range(10)|random }} days?

+

Ok, kidding, just type "yes" into the below box.

+ {{ copr_delete_form(form, copr) }} + +{% endblock %} diff --git a/frontend/coprs_frontend/coprs/templates/coprs/detail/edit.html b/frontend/coprs_frontend/coprs/templates/coprs/detail/edit.html new file mode 100644 index 0000000..9b82fe6 --- /dev/null +++ b/frontend/coprs_frontend/coprs/templates/coprs/detail/edit.html @@ -0,0 +1,10 @@ +{% extends "coprs/detail.html" %} +{% block title %}Editing {{ copr.owner.name }}/{{ copr.name }}{% endblock %} +{% block edit_selected %}selected{% endblock %} +{% from "coprs/_coprs_forms.html" import copr_form, copr_permissions_form with context %} + +{% block detail_body %} + + {{ copr_form(form, view = 'coprs_ns.copr_update', copr = copr) }} + +{% endblock %} diff --git a/frontend/coprs_frontend/coprs/templates/coprs/detail/edit2.html b/frontend/coprs_frontend/coprs/templates/coprs/detail/edit2.html new file mode 100644 index 0000000..3127b20 --- /dev/null +++ b/frontend/coprs_frontend/coprs/templates/coprs/detail/edit2.html @@ -0,0 +1,13 @@ +{% extends "coprs/detail.html" %} + +{% block detail_body %} + +
+ +
+ {% block detail_body2 %}{% endblock %} +{% endblock %} diff --git a/frontend/coprs_frontend/coprs/templates/coprs/detail/edit_chroot.html b/frontend/coprs_frontend/coprs/templates/coprs/detail/edit_chroot.html new file mode 100644 index 0000000..fac9a65 --- /dev/null +++ b/frontend/coprs_frontend/coprs/templates/coprs/detail/edit_chroot.html @@ -0,0 +1,19 @@ +{% extends "coprs/detail/edit2.html" %} +{% from "_helpers.html" import render_field %} +{% block title %}Editing {{ copr.owner.name }}/{{ copr.name }}/{{ chroot.name }}{% endblock %} +{% block edit_selected %}selected{% endblock %} +{% block edit_chroot_selected %}selected{% endblock %} + +{% block detail_body2 %} + +

Edit chroot '{{ chroot.name }}'

+
+
+ {{ form.csrf_token }} + {{ render_field(form.buildroot_pkgs, size=80, placeholder='Space separated list of packages. E.g.: scl-utils-build ruby193-build') }} +
+
+
+ + +{% endblock %} diff --git a/frontend/coprs_frontend/coprs/templates/coprs/detail/monitor.html b/frontend/coprs_frontend/coprs/templates/coprs/detail/monitor.html new file mode 100644 index 0000000..a2d9ab6 --- /dev/null +++ b/frontend/coprs_frontend/coprs/templates/coprs/detail/monitor.html @@ -0,0 +1,41 @@ +{% extends "coprs/detail.html" %} +{% block title %}Monitor {{ copr.owner.name }}/{{ copr.name }}{% endblock %} +{% block monitor_selected %}selected{% endblock %} + +{% block detail_body %} + {% if build %} +

+ Latest build status: + {{ build.state }} +

+ + + + {% for chroot in chroots %} + + {% endfor %} + + {% for package, states in packages %} + + + {% for build_id, state in states %} + + {% endfor %} + + {% endfor %} +
Package + {{ chroot }} +
{{ package }} + {% if state %} + {{ state }} + {% else %} + resubmit + {% endif %} +
+ {% else %} +

No builds so far.

+ {% endif %} +{% endblock %} diff --git a/frontend/coprs_frontend/coprs/templates/coprs/detail/overview.html b/frontend/coprs_frontend/coprs/templates/coprs/detail/overview.html new file mode 100644 index 0000000..d88cb12 --- /dev/null +++ b/frontend/coprs_frontend/coprs/templates/coprs/detail/overview.html @@ -0,0 +1,76 @@ +{% extends "coprs/detail.html" %} + +{% from "coprs/_coprs_forms.html" import copr_legal_flag_form with context %} + +{% block overview_selected %}selected{% endblock %} + +{% block detail_body %} +

Description

+
{{ copr.description|markdown|default('Description not filled in by author. Very likely personal repository for testing purpose, which you should not use.', true) }}
+

Installation Instructions

+
{{ copr.instructions|markdown|default('Instructions not filled in by author. Author knows what to do. Everybody else should avoid this repo.', true) }}
+

Active Releases

+
+

+ The following unofficial repositories are provided as-is by owner of this project. + Contact the owner directly for bugs or issues (IE: not bugzilla). +

+
+ + + + + + + {% for mock_chroot in copr.active_chroots %} + {% if loop.index < copr.active_chroots|length %} + {% if mock_chroot.os_release != copr.active_chroots[loop.index].os_release or + mock_chroot.os_version != copr.active_chroots[loop.index].os_version %} + {# next release is different => release-end #} + + {% else %} + + {% endif %} + {% else %}{# last line => release-end for sure #} + + {% endif %} + {% if mock_chroot.os_release != copr.active_chroots[loop.index0 - 1].os_release or + mock_chroot.os_version != copr.active_chroots[loop.index0 - 1].os_version or + loop.index0 == 0 %} + {# previous os_release-os_version were different or this is the first one #} + + {% else %} + + {% endif %} + + {% if mock_chroot.os_release != copr.active_chroots[loop.index0 - 1].os_release or + mock_chroot.os_version != copr.active_chroots[loop.index0 - 1].os_version or + loop.index0 == 0 %} + {# previous os_release-os_version were different or this is the first one #} + + {% else %} + + {% endif %} + + + {% else %} + + {% endfor %} +
ReleaseArchitectureYum Repo
{{ mock_chroot.os_release|capitalize }} {{ mock_chroot.os_version }}{{ mock_chroot.arch }} + {{ copr.owner.name }}-{{ copr.name }}.repo
No active releases
+ {% if copr.repos_list %} +

Repository List

+
    + {% for repo in copr.repos_list %} +
  • {{ repo }}
  • + {% endfor %} +
+ {% endif %} + +
+ {{ copr_legal_flag_form(form, copr) }} +{% endblock %} diff --git a/frontend/coprs_frontend/coprs/templates/coprs/detail/permissions.html b/frontend/coprs_frontend/coprs/templates/coprs/detail/permissions.html new file mode 100644 index 0000000..3d9cb75 --- /dev/null +++ b/frontend/coprs_frontend/coprs/templates/coprs/detail/permissions.html @@ -0,0 +1,13 @@ +{% extends "coprs/detail.html" %} +{% block title %}Permissions for {{ copr.owner.name }}/{{ copr.name }}{% endblock %} +{% block permissions_selected %}selected{% endblock %} +{% from "coprs/detail/_permissions_table.html" import permissions_table with context%} + +{% block detail_body %} + {% if (g.user and g.user != copr.owner) or permissions %} + {# the table is displayed only if there are some permissions or a non-owner is viewing the page (then display at least his applier form #} + {{ permissions_table(permissions, current_user_permissions, copr, permissions_applier_form, permissions_form) }} + {% else %} +

No permissions yet

+ {% endif %} +{% endblock %} diff --git a/frontend/coprs_frontend/coprs/templates/coprs/show.html b/frontend/coprs_frontend/coprs/templates/coprs/show.html new file mode 100644 index 0000000..a3d041e --- /dev/null +++ b/frontend/coprs_frontend/coprs/templates/coprs/show.html @@ -0,0 +1,61 @@ +{% extends "layout.html" %} +{% block title %}Project List{% endblock %} +{% block header %}Project List{% endblock %} +{% from "_helpers.html" import render_pagination %} +{% block body %} + {% if g.user %} + + {% endif %} + {% if not g.user and not fulltext%} +
+

Copr is an easy-to-use automatic build system providing a package repository as its output.

+

Start with making your own repository in these three steps:

+
    +
  1. choose an architecture and system you want to build for
  2. +
  3. provide Copr with src.rpm packages available online
  4. +
  5. let Coper do all the work and wait for your new repo
  6. +
+

For more information please visit Copr wiki

+
+ {% endif %} + +
+ {% if g.user %} + + {% endif %} + {% if fulltext %} +
Displaying results for search "{{ fulltext }}"
+ {% endif %} + {% for copr in coprs %} +
+ {{ copr.owner.name }}/{{ copr.name }} +

{{ copr.description|markdown|default('Description not filled in by author. Very likely personal repository for testing purpose, which you should not use.', true) }}

+

+ {% for mock_chroot in copr.active_chroots %} + {{ mock_chroot.os_release|os_name_short(mock_chroot.os_version) }}.{{ mock_chroot.arch }}{% if not loop.last %}, {% endif %} + {% endfor %} +

+
+ {% else %} +

No projects...

+ {% endfor %} + +
+{% endblock %} diff --git a/frontend/coprs_frontend/coprs/templates/layout.html b/frontend/coprs_frontend/coprs/templates/layout.html new file mode 100644 index 0000000..5a607bc --- /dev/null +++ b/frontend/coprs_frontend/coprs/templates/layout.html @@ -0,0 +1,45 @@ + + + + {% block title %}Coprs Build System{% endblock %} + + + + + + + + +
+ {% for message in get_flashed_messages() %} +
{{ message }}
+ {% endfor %} +
+ {% block body %}{% endblock %} +
+
+ + + diff --git a/frontend/coprs_frontend/coprs/templates/login.html b/frontend/coprs_frontend/coprs/templates/login.html new file mode 100644 index 0000000..7cad08a --- /dev/null +++ b/frontend/coprs_frontend/coprs/templates/login.html @@ -0,0 +1,16 @@ +{% extends "layout.html" %} +{% block title %}Sign in Coprs{% endblock %} +{% block header %}Sign in Coprs Build System{% endblock %} +{% block body %} + {% if error %}

Error: {{ error }}

{% endif %} + +
+

Fedora Accounts System login

+
+ Username: + + + +
+
+{% endblock %} diff --git a/frontend/coprs_frontend/coprs/views/__init__.py b/frontend/coprs_frontend/coprs/views/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/frontend/coprs_frontend/coprs/views/__init__.py diff --git a/frontend/coprs_frontend/coprs/views/admin_ns/__init__.py b/frontend/coprs_frontend/coprs/views/admin_ns/__init__.py new file mode 100644 index 0000000..8820bd1 --- /dev/null +++ b/frontend/coprs_frontend/coprs/views/admin_ns/__init__.py @@ -0,0 +1,3 @@ +import flask + +admin_ns = flask.Blueprint("admin_ns", __name__, url_prefix="/admin") diff --git a/frontend/coprs_frontend/coprs/views/admin_ns/admin_general.py b/frontend/coprs_frontend/coprs/views/admin_ns/admin_general.py new file mode 100644 index 0000000..1f91370 --- /dev/null +++ b/frontend/coprs_frontend/coprs/views/admin_ns/admin_general.py @@ -0,0 +1,44 @@ +import time + +import flask + +from coprs import db +from coprs import helpers +from coprs import models + +from coprs.views.admin_ns import admin_ns +from coprs.views.misc import login_required + + +@admin_ns.route("/") +@login_required(role=helpers.RoleEnum("admin")) +def admin_index(): + return flask.render_template("admin/index.html") + + +@admin_ns.route("/legal-flag/") +@login_required(role=helpers.RoleEnum("admin")) +def legal_flag(): + legal_flags = (models.LegalFlag.query + .outerjoin(models.LegalFlag.copr) + .options(db.contains_eager(models.LegalFlag.copr)) + .filter(models.LegalFlag.resolved_on == None) + .order_by(models.LegalFlag.raised_on.desc()) + .all()) + + return flask.render_template("admin/legal-flag.html", + legal_flags=legal_flags) + + +@admin_ns.route("/legal-flag//resolve/", methods=["POST"]) +@login_required(role=helpers.RoleEnum("admin")) +def legal_flag_resolve(flag_id): + + (models.LegalFlag.query + .filter(models.LegalFlag.id == flag_id) + .update({"resolved_on": int(time.time()), + "resolver_id": flask.g.user.id})) + + db.session.commit() + flask.flash("Legal flag resolved") + return flask.redirect(flask.url_for("admin_ns.legal_flag")) diff --git a/frontend/coprs_frontend/coprs/views/api_ns/__init__.py b/frontend/coprs_frontend/coprs/views/api_ns/__init__.py new file mode 100644 index 0000000..11e81ae --- /dev/null +++ b/frontend/coprs_frontend/coprs/views/api_ns/__init__.py @@ -0,0 +1,3 @@ +import flask + +api_ns = flask.Blueprint("api_ns", __name__, url_prefix="/api") diff --git a/frontend/coprs_frontend/coprs/views/api_ns/api_general.py b/frontend/coprs_frontend/coprs/views/api_ns/api_general.py new file mode 100644 index 0000000..f792977 --- /dev/null +++ b/frontend/coprs_frontend/coprs/views/api_ns/api_general.py @@ -0,0 +1,418 @@ +import base64 +import datetime +import urlparse + +import flask + +from coprs import db +from coprs import exceptions +from coprs import forms +from coprs import helpers + +from coprs.views.misc import login_required, api_login_required + +from coprs.views.api_ns import api_ns + +from coprs.logic import builds_logic +from coprs.logic import coprs_logic + + +@api_ns.route("/") +def api_home(): + """ + Render the home page of the api. + This page provides information on how to call/use the API. + """ + + return flask.render_template("api.html") + + +@api_ns.route("/new/", methods=["GET", "POST"]) +@login_required +def api_new_token(): + """ + Generate a new API token for the current user. + """ + + user = flask.g.user + copr64 = base64.b64encode("copr") + "##" + api_login = helpers.generate_api_token( + flask.current_app.config["API_TOKEN_LENGTH"] - len(copr64)) + user.api_login = api_login + user.api_token = helpers.generate_api_token( + flask.current_app.config["API_TOKEN_LENGTH"]) + user.api_token_expiration = datetime.date.today() + \ + datetime.timedelta( + days=flask.current_app.config["API_TOKEN_EXPIRATION"]) + + db.session.add(user) + db.session.commit() + return flask.redirect(flask.url_for("api_ns.api_home")) + + +@api_ns.route("/coprs//new/", methods=["POST"]) +@api_login_required +def api_new_copr(username): + """ + Receive information from the user on how to create its new copr, + check their validity and create the corresponding copr. + + :arg name: the name of the copr to add + :arg chroots: a comma separated list of chroots to use + :kwarg repos: a comma separated list of repository that this copr + can use. + :kwarg initial_pkgs: a comma separated list of initial packages to + build in this new copr + + """ + + form = forms.CoprFormFactory.create_form_cls()(csrf_enabled=False) + httpcode = 200 + if form.validate_on_submit(): + infos = [] + try: + copr = coprs_logic.CoprsLogic.add( + name=form.name.data.strip(), + repos=" ".join(form.repos.data.split()), + user=flask.g.user, + selected_chroots=form.selected_chroots, + description=form.description.data, + instructions=form.instructions.data, + check_for_duplicates=True) + infos.append("New project was successfully created.") + + if form.initial_pkgs.data: + builds_logic.BuildsLogic.add( + user=flask.g.user, + pkgs=" ".join(form.initial_pkgs.data.split()), + copr=copr) + + infos.append("Initial packages were successfully " + "submitted for building.") + + output = {"output": "ok", "message": "\n".join(infos)} + db.session.commit() + except exceptions.DuplicateException as err: + output = {"output": "notok", "error": err} + httpcode = 500 + db.session.rollback() + + else: + errormsg = "Validation error\n" + if form.errors: + for field, emsgs in form.errors.items(): + errormsg += "- {0}: {1}\n".format(field, "\n".join(emsgs)) + + errormsg = errormsg.replace('"', "'") + output = {"output": "notok", "error": errormsg} + httpcode = 500 + + jsonout = flask.jsonify(output) + jsonout.status_code = httpcode + return jsonout + + +@api_ns.route("/coprs/") +@api_ns.route("/coprs//") +def api_coprs_by_owner(username=None): + """ Return the list of coprs owned by the given user. + username is taken either from GET params or from the URL itself + (in this order). + + :arg username: the username of the person one would like to the + coprs of. + + """ + username = flask.request.args.get("username", None) or username + release_tmpl = "{chroot.os_release}-{chroot.os_version}-{chroot.arch}" + httpcode = 200 + if username: + query = coprs_logic.CoprsLogic.get_multiple( + flask.g.user, user_relation="owned", + username=username, with_builds=True) + + repos = query.all() + output = {"output": "ok", "repos": []} + for repo in repos: + yum_repos = {} + for build in repo.builds: + if build.results: + for chroot in repo.active_chroots: + release = release_tmpl.format(chroot=chroot) + yum_repos[release] = urlparse.urljoin( + build.results, release + '/') + break + + output["repos"].append({"name": repo.name, + "additional_repos": repo.repos, + "yum_repos": yum_repos, + "description": repo.description, + "instructions": repo.instructions}) + else: + output = {"output": "notok", "error": "Invalid request"} + httpcode = 500 + + jsonout = flask.jsonify(output) + jsonout.status_code = httpcode + return jsonout + +@api_ns.route("/coprs///detail/") +def api_coprs_by_owner_detail(username, coprname): + """ Return detail of one project. + + :arg username: the username of the person one would like to the + coprs of. + :arg coprname: the name of project. + + """ + copr = coprs_logic.CoprsLogic.get(flask.g.user, username, + coprname).first() + release_tmpl = "{chroot.os_release}-{chroot.os_version}-{chroot.arch}" + httpcode = 200 + if username and copr: + output = {"output": "ok", "detail": {}} + yum_repos = {} + for build in copr.builds: + if build.results: + for chroot in copr.active_chroots: + release = release_tmpl.format(chroot=chroot) + yum_repos[release] = urlparse.urljoin( + build.results, release + '/') + break + output["detail"] = {"name": copr.name, + "additional_repos": copr.repos, + "yum_repos": yum_repos, + "description": copr.description, + "instructions": copr.instructions, + "last_modified": builds_logic.BuildsLogic.last_modified(copr)} + else: + output = {"output": "notok", "error": "Copr with name {0} does not exist.".format(coprname)} + httpcode = 500 + + jsonout = flask.jsonify(output) + jsonout.status_code = httpcode + return jsonout + +@api_ns.route("/coprs///new_build/", methods=["POST"]) +@api_login_required +def copr_new_build(username, coprname): + form = forms.BuildForm(csrf_enabled=False) + copr = coprs_logic.CoprsLogic.get(flask.g.user, username, + coprname).first() + httpcode = 200 + if not copr: + output = {"output": "notok", "error": + "Copr with name {0} does not exist.".format(coprname)} + httpcode = 500 + + else: + if form.validate_on_submit() and flask.g.user.can_build_in(copr): + # we're checking authorization above for now + build = builds_logic.BuildsLogic.add( + user=flask.g.user, + pkgs=form.pkgs.data.replace('\n', ' '), + copr=copr) + + if flask.g.user.proven: + build.memory_reqs = form.memory_reqs.data + build.timeout = form.timeout.data + + db.session.commit() + + output = {"output": "ok", + "id": build.id, + "message": "Build was added to {0}.".format(coprname)} + else: + output = {"output": "notok", "error": "Invalid request"} + httpcode = 500 + + jsonout = flask.jsonify(output) + jsonout.status_code = httpcode + return jsonout + + +@api_ns.route("/coprs/build_status//", methods=["GET"]) +@api_login_required +def build_status(build_id): + if build_id.isdigit(): + build = builds_logic.BuildsLogic.get(build_id).first() + else: + build = None + + if build: + httpcode = 200 + output = {"output": "ok", + "status": build.state} + else: + output = {"output": "notok", "error": "Invalid build"} + httpcode = 404 + + jsonout = flask.jsonify(output) + jsonout.status_code = httpcode + return jsonout + +@api_ns.route("/coprs/build_detail//", methods=["GET"]) +def build_detail(build_id): + if build_id.isdigit(): + build = builds_logic.BuildsLogic.get(build_id).first() + else: + build = None + + if build: + httpcode = 200 + output = {"output": "ok", + "owner": build.copr.owner.name, + "project": build.copr.name, + "status": build.state} + else: + output = {"output": "notok", "error": "Invalid build"} + httpcode = 404 + + jsonout = flask.jsonify(output) + jsonout.status_code = httpcode + return jsonout + +@api_ns.route("/coprs/cancel_build//", methods=["POST"]) +@api_login_required +def cancel_build(build_id): + if build_id.isdigit(): + build = builds_logic.BuildsLogic.get(build_id).first() + else: + build = None + + if build: + try: + builds_logic.BuildsLogic.cancel_build(flask.g.user, build) + except InsufficientRightsException as e: + output = {'output': 'notok', 'error': str(e)} + httpcode = 500 + else: + db.session.commit() + httpcode = 200 + output = {'output': 'ok', status: "Build canceled"} + else: + output = {"output": "notok", "error": "Invalid build"} + httpcode = 404 + jsonout = flask.jsonify(output) + jsonout.status_code = httpcode + return jsonout + +@api_ns.route('/coprs///modify/', methods=["POST"]) +@api_login_required +def copr_modify(username, coprname): + form = forms.CoprModifyForm(csrf_enabled=False) + copr = coprs_logic.CoprsLogic.get(flask.g.user, username, coprname).first() + + if copr is None: + output = {'output': 'notok', 'error': 'Invalid copr name or username'} + httpcode = 500 + elif not form.validate_on_submit(): + output = {'output': 'notok', 'error': 'Invalid request'} + httpcode = 500 + else: + # .raw_data needs to be inspected to figure out whether the field + # was not sent or was sent empty + if form.description.raw_data and len(form.description.raw_data): + copr.description = form.description.data + if form.instructions.raw_data and len(form.instructions.raw_data): + copr.instructions = form.instructions.data + if form.repos.raw_data and len(form.repos.raw_data): + copr.repos = form.repos.data + + try: + coprs_logic.CoprsLogic.update(flask.g.user, copr) + except (exceptions.ActionInProgressException, exceptions.InsufficientRightsException) as e: + db.session.rollback() + + output = {'output': 'notok', 'error': str(e)} + httpcode = 500 + else: + db.session.commit() + + output = {'output': 'ok', + 'description': copr.description, + 'instructions': copr.instructions, + 'repos': copr.repos} + httpcode = 200 + + jsonout = flask.jsonify(output) + jsonout.status_code = httpcode + return jsonout + +@api_ns.route('/coprs///modify//', methods=["POST"]) +@api_login_required +def copr_modify_chroot(username, coprname, chrootname): + form = forms.ModifyChrootForm(csrf_enabled=False) + copr = coprs_logic.CoprsLogic.get(flask.g.user, username, coprname).first() + chroot = coprs_logic.MockChrootsLogic.get_from_name(chrootname, active_only=True).first() + + if copr is None: + output = {'output': 'notok', 'error': 'Invalid copr name or username'} + httpcode = 500 + elif chroot is None: + output = {'output': 'notok', 'error': 'Invalid chroot name'} + httpcode = 500 + elif not form.validate_on_submit(): + output = {'output': 'notok', 'error': 'Invalid request'} + httpcode = 500 + else: + coprs_logic.CoprChrootsLogic.update_buildroot_pkgs(copr, chroot, form.buildroot_pkgs.data) + db.session.commit() + + ch = copr.check_copr_chroot(chroot) + output = {'output': 'ok', 'buildroot_pkgs': ch.buildroot_pkgs} + httpcode = 200 + + jsonout = flask.jsonify(output) + jsonout.status_code = httpcode + return jsonout + +@api_ns.route('/coprs///detail//', methods=["GET"]) +def copr_chroot_details(username, coprname, chrootname): + copr = coprs_logic.CoprsLogic.get(flask.g.user, username, coprname).first() + chroot = coprs_logic.MockChrootsLogic.get_from_name(chrootname, active_only=True).first() + + if copr is None: + output = {'output': 'notok', 'error': 'Invalid copr name or username'} + httpcode = 500 + elif chroot is None: + output = {'output': 'notok', 'error': 'Invalid chroot name'} + httpcode = 500 + else: + ch = copr.check_copr_chroot(chroot) + output = {'output': 'ok', 'buildroot_pkgs': ch.buildroot_pkgs} + httpcode = 200 + + jsonout = flask.jsonify(output) + jsonout.status_code = httpcode + return jsonout + +@api_ns.route("/coprs/search/") +@api_ns.route("/coprs/search//") +def api_coprs_search_by_project(project=None): + """ Return the list of coprs found in search by the given text. + project is taken either from GET params or from the URL itself + (in this order). + + :arg project: the text one would like find for coprs. + + """ + project = flask.request.args.get("project", None) or project + httpcode = 200 + if project: + query = coprs_logic.CoprsLogic.get_multiple_fulltext( + flask.g.user, project) + + repos = query.all() + output = {"output": "ok", "users": []} + for repo in repos: + output["repos"].append({"username": repo.owner, + "coprname": repo.name, + "description": repo.description}) + else: + output = {"output": "notok", "error": "Invalid request"} + httpcode = 500 + + jsonout = flask.jsonify(output) + jsonout.status_code = httpcode + return jsonout diff --git a/frontend/coprs_frontend/coprs/views/backend_ns/__init__.py b/frontend/coprs_frontend/coprs/views/backend_ns/__init__.py new file mode 100644 index 0000000..f527c95 --- /dev/null +++ b/frontend/coprs_frontend/coprs/views/backend_ns/__init__.py @@ -0,0 +1,3 @@ +import flask + +backend_ns = flask.Blueprint("backend_ns", __name__, url_prefix="/backend") diff --git a/frontend/coprs_frontend/coprs/views/backend_ns/backend_general.py b/frontend/coprs_frontend/coprs/views/backend_ns/backend_general.py new file mode 100644 index 0000000..1a9f473 --- /dev/null +++ b/frontend/coprs_frontend/coprs/views/backend_ns/backend_general.py @@ -0,0 +1,90 @@ +import flask +import sys +import time + +from coprs import db +from coprs.logic import actions_logic +from coprs.logic import builds_logic + +from coprs.views import misc +from coprs.views.backend_ns import backend_ns +from whoosh.index import LockError + + +@backend_ns.route("/waiting/") +@misc.backend_authenticated +def waiting(): + """ + Return list of waiting actions and builds. + """ + + # models.Actions + actions_list = [action.to_dict( + options={"__columns_except__": ["result", "message", "ended_on"]}) + for action in actions_logic.ActionsLogic.get_waiting() + ] + + # models.Builds + builds_list = [] + + for build in builds_logic.BuildsLogic.get_waiting(): + build_dict = build.to_dict( + options={"copr": {"owner": {}, + "__columns_only__": ["id", "name"], + "__included_ids__": False + }, + "__included_ids__": False}) + + # return separate build for each chroot this build + # is assigned with + for chroot in build.chroots: + build_dict_copy = build_dict.copy() + build_dict_copy["chroot"] = chroot.name + build_dict_copy[ + "buildroot_pkgs"] = build.copr.buildroot_pkgs(chroot) + builds_list.append(build_dict_copy) + + return flask.jsonify({"actions": actions_list, "builds": builds_list}) + + +@backend_ns.route("/update/", methods=["POST", "PUT"]) +@misc.backend_authenticated +def update(): + result = {} + + for typ, logic_cls in [("actions", actions_logic.ActionsLogic), + ("builds", builds_logic.BuildsLogic)]: + + if typ not in flask.request.json: + continue + + to_update = {} + for obj in flask.request.json[typ]: + to_update[obj["id"]] = obj + + existing = {} + for obj in logic_cls.get_by_ids(to_update.keys()).all(): + existing[obj.id] = obj + + non_existing_ids = list(set(to_update.keys()) - set(existing.keys())) + + for i, obj in existing.items(): + logic_cls.update_state_from_dict(obj, to_update[i]) + + i = 5 + exc_info = None + while i > 0: + try: + db.session.commit() + i = -100 + except LockError: + i -= 1 + exc_info = sys.exc_info()[2] + time.sleep(5) + if i != -100: + raise LockError, None, exc_info + + result.update({"updated_{0}_ids".format(typ): list(existing.keys()), + "non_existing_{0}_ids".format(typ): non_existing_ids}) + + return flask.jsonify(result) diff --git a/frontend/coprs_frontend/coprs/views/coprs_ns/__init__.py b/frontend/coprs_frontend/coprs/views/coprs_ns/__init__.py new file mode 100644 index 0000000..94372a2 --- /dev/null +++ b/frontend/coprs_frontend/coprs/views/coprs_ns/__init__.py @@ -0,0 +1,3 @@ +import flask + +coprs_ns = flask.Blueprint("coprs_ns", __name__, url_prefix="/coprs") diff --git a/frontend/coprs_frontend/coprs/views/coprs_ns/coprs_builds.py b/frontend/coprs_frontend/coprs/views/coprs_ns/coprs_builds.py new file mode 100644 index 0000000..dbcc3dd --- /dev/null +++ b/frontend/coprs_frontend/coprs/views/coprs_ns/coprs_builds.py @@ -0,0 +1,176 @@ +import flask + +from coprs import db +from coprs import forms +from coprs import helpers + +from coprs.logic import builds_logic +from coprs.logic import coprs_logic + +from coprs.views.misc import login_required, page_not_found +from coprs.views.coprs_ns import coprs_ns + +from coprs.exceptions import (ActionInProgressException, + InsufficientRightsException) + + +@coprs_ns.route("///builds/", defaults={"page": 1}) +@coprs_ns.route("///builds//") +def copr_builds(username, coprname, page=1): + copr = coprs_logic.CoprsLogic.get(flask.g.user, username, coprname).first() + + if not copr: + return page_not_found( + "Copr with name {0} does not exist.".format(coprname)) + + builds_query = builds_logic.BuildsLogic.get_multiple( + flask.g.user, copr=copr) + + paginator = helpers.Paginator( + builds_query, copr.build_count, page, per_page_override=10) + + return flask.render_template("coprs/detail/builds.html", + copr=copr, + builds=paginator.sliced_query, + paginator=paginator) + + +@coprs_ns.route("///add_build/") +@login_required +def copr_add_build(username, coprname, form=None): + copr = coprs_logic.CoprsLogic.get(flask.g.user, username, coprname).first() + + if not copr: + return page_not_found( + "Copr with name {0} does not exist.".format(coprname)) + + if not form: + form = forms.BuildForm() + + return flask.render_template("coprs/detail/add_build.html", + copr=copr, + form=form) + + +@coprs_ns.route("///new_build/", methods=["POST"]) +@login_required +def copr_new_build(username, coprname): + form = forms.BuildForm() + copr = coprs_logic.CoprsLogic.get(flask.g.user, username, coprname).first() + if not copr: + return page_not_found( + "Copr with name {0} does not exist.".format(coprname)) + + if form.validate_on_submit(): + try: + build = builds_logic.BuildsLogic.add(user=flask.g.user, + pkgs=form.pkgs.data.replace( + "\n", " "), + copr=copr) + if flask.g.user.proven: + build.memory_reqs = form.memory_reqs.data + build.timeout = form.timeout.data + + except (ActionInProgressException, InsufficientRightsException) as e: + flask.flash(str(e)) + db.session.rollback() + else: + flask.flash("Build was added") + db.session.commit() + + return flask.redirect(flask.url_for("coprs_ns.copr_builds", + username=username, + coprname=copr.name)) + else: + return copr_add_build(username=username, coprname=coprname, form=form) + + +@coprs_ns.route("///cancel_build//", + defaults={"page": 1}, + methods=["POST"]) +@coprs_ns.route("///cancel_build///", + methods=["POST"]) +@login_required +def copr_cancel_build(username, coprname, build_id, page=1): + # only the user who ran the build can cancel it + build = builds_logic.BuildsLogic.get(build_id).first() + if not build: + return page_not_found( + "Build with id {0} does not exist.".format(build_id)) + try: + builds_logic.BuildsLogic.cancel_build(flask.g.user, build) + except InsufficientRightsException as e: + flask.flash(str(e)) + else: + db.session.commit() + flask.flash("Build was canceled") + + return flask.redirect(flask.url_for("coprs_ns.copr_builds", + username=username, + coprname=coprname, + page=page)) + + +@coprs_ns.route("///repeat_build//", + defaults={"page": 1}, + methods=["GET", "POST"]) +@coprs_ns.route("///repeat_build///", + methods=["GET", "POST"]) +@login_required +def copr_repeat_build(username, coprname, build_id, page=1): + build = builds_logic.BuildsLogic.get(build_id).first() + copr = coprs_logic.CoprsLogic.get( + flask.g.user, username=username, coprname=coprname).first() + + if not build: + return page_not_found( + "Build with id {0} does not exist.".format(build_id)) + + if not copr: + return page_not_found( + "Copr {0}/{1} does not exist.".format(username, coprname)) + + try: + builds_logic.BuildsLogic.add( + user=flask.g.user, + pkgs=build.pkgs, + copr=copr, + repos=build.repos, + memory_reqs=build.memory_reqs, + timeout=build.timeout) + + except (ActionInProgressException, InsufficientRightsException) as e: + db.session.rollback() + flask.flash(str(e)) + else: + db.session.commit() + flask.flash("Build was resubmitted") + + return flask.redirect(flask.url_for("coprs_ns.copr_builds", + username=username, + coprname=coprname, + page=page)) + + +@coprs_ns.route("///delete_build//", + defaults={"page": 1}, + methods=["POST"]) +@coprs_ns.route("///delete_build///", + methods=["POST"]) +@login_required +def copr_delete_build(username, coprname, build_id, page=1): + build = builds_logic.BuildsLogic.get(build_id).first() + if not build: + return page_not_found( + "Build with id {0} does not exist.".format(build_id)) + try: + builds_logic.BuildsLogic.delete_build(flask.g.user, build) + except InsufficientRightsException as e: + flask.flash(str(e)) + else: + db.session.commit() + flask.flash("Build was deleted") + + return flask.redirect(flask.url_for("coprs_ns.copr_builds", + username=username, coprname=coprname, + page=page)) diff --git a/frontend/coprs_frontend/coprs/views/coprs_ns/coprs_chroots.py b/frontend/coprs_frontend/coprs/views/coprs_ns/coprs_chroots.py new file mode 100644 index 0000000..21c5267 --- /dev/null +++ b/frontend/coprs_frontend/coprs/views/coprs_ns/coprs_chroots.py @@ -0,0 +1,75 @@ +import flask + +from coprs import db +from coprs import forms + +from coprs.logic import coprs_logic + +from coprs.views.misc import login_required, page_not_found +from coprs.views.coprs_ns import coprs_ns + + +@coprs_ns.route("///edit_chroot//") +@login_required +def chroot_edit(username, coprname, chrootname): + copr = coprs_logic.CoprsLogic.get(flask.g.user, username, coprname).first() + if not copr: + return page_not_found( + "Project with name {0} does not exist.".format(coprname)) + + try: + chroot = coprs_logic.MockChrootsLogic.get_from_name( + chrootname, active_only=True).first() + except ValueError as e: + return page_not_found(str(e)) + + if not chroot: + return page_not_found( + "Chroot name {0} does not exist.".format(chrootname)) + + form = forms.ChrootForm(buildroot_pkgs=copr.buildroot_pkgs(chroot)) + # FIXME - test if chroot belongs to copr + if flask.g.user.can_build_in(copr): + return flask.render_template("coprs/detail/edit_chroot.html", + form=form, copr=copr, chroot=chroot) + else: + return page_not_found( + "You are not allowed to modify chroots in project {0}." + .format(coprname)) + + +@coprs_ns.route("///update_chroot//", + methods=["POST"]) +@login_required +def chroot_update(username, coprname, chrootname): + form = forms.ChrootForm() + copr = coprs_logic.CoprsLogic.get(flask.g.user, username, coprname).first() + if not copr: + return page_not_found( + "Projec with name {0} does not exist.".format(coprname)) + + try: + chroot = coprs_logic.MockChrootsLogic.get_from_name( + chrootname, active_only=True).first() + except ValueError as e: + return page_not_found(str(e)) + + if form.validate_on_submit() and flask.g.user.can_build_in(copr): + coprs_logic.CoprChrootsLogic.update_buildroot_pkgs( + copr, chroot, form.buildroot_pkgs.data) + + flask.flash( + "Buildroot {0} for project {1} was updated".format( + chrootname, coprname)) + + db.session.commit() + + return flask.redirect(flask.url_for("coprs_ns.copr_edit", + username=username, + coprname=copr.name)) + + else: + if form.validate_on_submit(): + flask.flash("You are not allowed to modify chroots.") + else: + return chroot_edit(username, coprname, chrootname) diff --git a/frontend/coprs_frontend/coprs/views/coprs_ns/coprs_general.py b/frontend/coprs_frontend/coprs/views/coprs_ns/coprs_general.py new file mode 100644 index 0000000..cf56e12 --- /dev/null +++ b/frontend/coprs_frontend/coprs/views/coprs_ns/coprs_general.py @@ -0,0 +1,502 @@ +import os +import time + +import flask +import platform +import smtplib +import sqlalchemy +from email.mime.text import MIMEText + +from coprs import app +from coprs import db +from coprs import exceptions +from coprs import forms +from coprs import helpers +from coprs import models + +from coprs.views.misc import login_required, page_not_found + +from coprs.views.coprs_ns import coprs_ns + +from coprs.logic import builds_logic +from coprs.logic import coprs_logic +from coprs.helpers import parse_package_name, render_repo + + +@coprs_ns.route("/", defaults={"page": 1}) +@coprs_ns.route("//") +def coprs_show(page=1): + query = coprs_logic.CoprsLogic.get_multiple( + flask.g.user, with_mock_chroots=False) + paginator = helpers.Paginator(query, query.count(), page) + + coprs = paginator.sliced_query + return flask.render_template("coprs/show.html", + coprs=coprs, + paginator=paginator) + + +@coprs_ns.route("//", defaults={"page": 1}) +@coprs_ns.route("///") +def coprs_by_owner(username=None, page=1): + query = coprs_logic.CoprsLogic.get_multiple(flask.g.user, + user_relation="owned", + username=username, + with_mock_chroots=False) + + paginator = helpers.Paginator(query, query.count(), page) + + coprs = paginator.sliced_query + return flask.render_template("coprs/show.html", + coprs=coprs, + paginator=paginator) + + +@coprs_ns.route("//allowed/", defaults={"page": 1}) +@coprs_ns.route("//allowed//") +def coprs_by_allowed(username=None, page=1): + query = coprs_logic.CoprsLogic.get_multiple(flask.g.user, + user_relation="allowed", + username=username, + with_mock_chroots=False) + paginator = helpers.Paginator(query, query.count(), page) + + coprs = paginator.sliced_query + return flask.render_template("coprs/show.html", + coprs=coprs, + paginator=paginator) + + +@coprs_ns.route("/fulltext/", defaults={"page": 1}) +@coprs_ns.route("/fulltext//") +def coprs_fulltext_search(page=1): + fulltext = flask.request.args.get("fulltext", "") + try: + query = coprs_logic.CoprsLogic.get_multiple_fulltext( + flask.g.user, fulltext) + except ValueError as e: + flask.flash(str(e)) + return flask.redirect(flask.request.referrer or + flask.url_for("coprs_ns.coprs_show")) + + paginator = helpers.Paginator(query, query.count(), page) + + coprs = paginator.sliced_query + return flask.render_template("coprs/show.html", + coprs=coprs, + paginator=paginator, + fulltext=fulltext) + + +@coprs_ns.route("//add/") +@login_required +def copr_add(username): + form = forms.CoprFormFactory.create_form_cls()() + + return flask.render_template("coprs/add.html", form=form) + + +@coprs_ns.route("//new/", methods=["POST"]) +@login_required +def copr_new(username): + """ + Receive information from the user on how to create its new copr + and create it accordingly. + """ + + form = forms.CoprFormFactory.create_form_cls()() + if form.validate_on_submit(): + copr = coprs_logic.CoprsLogic.add( + flask.g.user, + name=form.name.data, + repos=form.repos.data.replace("\n", " "), + selected_chroots=form.selected_chroots, + description=form.description.data, + instructions=form.instructions.data) + + db.session.commit() + flask.flash("New project was successfully created.") + + if form.initial_pkgs.data: + builds_logic.BuildsLogic.add( + flask.g.user, + pkgs=form.initial_pkgs.data.replace("\n", " "), + copr=copr) + + db.session.commit() + flask.flash("Initial packages were successfully submitted " + "for building.") + + return flask.redirect(flask.url_for("coprs_ns.copr_detail", + username=flask.g.user.name, + coprname=copr.name)) + else: + return flask.render_template("coprs/add.html", form=form) + + +@coprs_ns.route("///") +def copr_detail(username, coprname): + query = coprs_logic.CoprsLogic.get( + flask.g.user, username, coprname, with_mock_chroots=True) + form = forms.CoprLegalFlagForm() + try: + copr = query.one() + except sqlalchemy.orm.exc.NoResultFound: + return page_not_found( + "Copr with name {0} does not exist.".format(coprname)) + + return flask.render_template("coprs/detail/overview.html", + copr=copr, + form=form) + + +@coprs_ns.route("///permissions/") +def copr_permissions(username, coprname): + query = coprs_logic.CoprsLogic.get(flask.g.user, username, coprname) + copr = query.first() + if not copr: + return page_not_found( + "Copr with name {0} does not exist.".format(coprname)) + + permissions = coprs_logic.CoprPermissionsLogic.get_for_copr( + flask.g.user, copr).all() + if flask.g.user: + user_perm = flask.g.user.permissions_for_copr(copr) + else: + user_perm = None + + permissions_applier_form = None + permissions_form = None + + # generate a proper form for displaying + if flask.g.user: + if flask.g.user.can_edit(copr): + permissions_form = forms.PermissionsFormFactory.create_form_cls( + permissions)() + else: + # https://github.com/ajford/flask-wtf/issues/58 + permissions_applier_form = \ + forms.PermissionsApplierFormFactory.create_form_cls( + user_perm)(formdata=None) + + return flask.render_template( + "coprs/detail/permissions.html", + copr=copr, + permissions_form=permissions_form, + permissions_applier_form=permissions_applier_form, + permissions=permissions, + current_user_permissions=user_perm) + + +@coprs_ns.route("///edit/") +@login_required +def copr_edit(username, coprname, form=None): + query = coprs_logic.CoprsLogic.get(flask.g.user, username, coprname) + copr = query.first() + + if not copr: + return page_not_found( + "Copr with name {0} does not exist.".format(coprname)) + + if not form: + form = forms.CoprFormFactory.create_form_cls( + copr.mock_chroots)(obj=copr) + + return flask.render_template("coprs/detail/edit.html", + copr=copr, + form=form) + + +@coprs_ns.route("///update/", methods=["POST"]) +@login_required +def copr_update(username, coprname): + form = forms.CoprFormFactory.create_form_cls()() + copr = coprs_logic.CoprsLogic.get(flask.g.user, username, coprname).first() + + if form.validate_on_submit(): + # we don"t change owner (yet) + copr.name = form.name.data + copr.repos = form.repos.data.replace("\n", " ") + copr.description = form.description.data + copr.instructions = form.instructions.data + coprs_logic.CoprChrootsLogic.update_from_names( + flask.g.user, copr, form.selected_chroots) + + try: + # form validation checks for duplicates + coprs_logic.CoprsLogic.update( + flask.g.user, copr, check_for_duplicates=False) + except (exceptions.ActionInProgressException, + exceptions.InsufficientRightsException) as e: + + flask.flash(str(e)) + db.session.rollback() + else: + flask.flash("Project was updated successfully.") + db.session.commit() + + return flask.redirect(flask.url_for("coprs_ns.copr_detail", + username=username, + coprname=copr.name)) + else: + return copr_edit(username, coprname, form) + + +@coprs_ns.route("///permissions_applier_change/", + methods=["POST"]) +@login_required +def copr_permissions_applier_change(username, coprname): + copr = coprs_logic.CoprsLogic.get(flask.g.user, username, coprname).first() + permission = coprs_logic.CoprPermissionsLogic.get( + flask.g.user, copr, flask.g.user).first() + applier_permissions_form = \ + forms.PermissionsApplierFormFactory.create_form_cls(permission)() + + if not copr: + return page_not_found( + "Project with name {0} does not exist.".format(coprname)) + + if copr.owner == flask.g.user: + flask.flash("Owner cannot request permissions for his own project.") + elif applier_permissions_form.validate_on_submit(): + # we rely on these to be 0 or 1 from form. TODO: abstract from that + new_builder = applier_permissions_form.copr_builder.data + new_admin = applier_permissions_form.copr_admin.data + coprs_logic.CoprPermissionsLogic.update_permissions_by_applier( + flask.g.user, copr, permission, new_builder, new_admin) + db.session.commit() + flask.flash( + "Successfuly updated permissions for project '{0}'." + .format(copr.name)) + + return flask.redirect(flask.url_for("coprs_ns.copr_detail", + username=copr.owner.name, + coprname=copr.name)) + + +@coprs_ns.route("///update_permissions/", methods=["POST"]) +@login_required +def copr_update_permissions(username, coprname): + query = coprs_logic.CoprsLogic.get(flask.g.user, username, coprname) + copr = query.first() + permissions = copr.copr_permissions + permissions_form = forms.PermissionsFormFactory.create_form_cls( + permissions)() + + if permissions_form.validate_on_submit(): + # we don't change owner (yet) + try: + # if admin is changing his permissions, his must be changed last + # so that we don't get InsufficientRightsException + permissions.sort( + cmp=lambda x, y: -1 if y.user_id == flask.g.user.id else 1) + for perm in permissions: + new_builder = permissions_form[ + "copr_builder_{0}".format(perm.user_id)].data + new_admin = permissions_form[ + "copr_admin_{0}".format(perm.user_id)].data + coprs_logic.CoprPermissionsLogic.update_permissions( + flask.g.user, copr, perm, new_builder, new_admin) + # for now, we don't check for actions here, as permissions operation + # don't collide with any actions + except exceptions.InsufficientRightsException as e: + db.session.rollback() + flask.flash(str(e)) + else: + db.session.commit() + flask.flash("Project permissions were updated successfully.") + + return flask.redirect(flask.url_for("coprs_ns.copr_detail", + username=copr.owner.name, + coprname=copr.name)) + + +@coprs_ns.route("///delete/", methods=["GET", "POST"]) +@login_required +def copr_delete(username, coprname): + form = forms.CoprDeleteForm() + copr = coprs_logic.CoprsLogic.get(flask.g.user, username, coprname).first() + + if form.validate_on_submit() and copr: + try: + coprs_logic.CoprsLogic.delete(flask.g.user, copr) + except (exceptions.ActionInProgressException, + exceptions.InsufficientRightsException) as e: + + db.session.rollback() + flask.flash(str(e)) + return flask.redirect(flask.url_for("coprs_ns.copr_detail", + username=username, + coprname=coprname)) + else: + db.session.commit() + flask.flash("Project was deleted successfully.") + return flask.redirect(flask.url_for("coprs_ns.coprs_by_owner", + username=username)) + else: + if copr: + return flask.render_template("coprs/detail/delete.html", + form=form, copr=copr) + else: + return page_not_found("Project {0}/{1} does not exist" + .format(username, coprname)) + + +@coprs_ns.route("///legal_flag/", methods=["POST"]) +@login_required +def copr_legal_flag(username, coprname): + form = forms.CoprLegalFlagForm() + copr = coprs_logic.CoprsLogic.get(flask.g.user, username, coprname).first() + + legal_flag = models.LegalFlag(raise_message=form.comment.data, + raised_on=int(time.time()), + copr=copr, + reporter=flask.g.user) + db.session.add(legal_flag) + db.session.commit() + + send_to = app.config["SEND_LEGAL_TO"] or ["root@localhost"] + hostname = platform.node() + navigate_to = "\nNavigate to http://{0}{1}".format( + hostname, flask.url_for("admin_ns.legal_flag")) + + contact = "\nContact on owner is: {0} <{1}>".format(username, + copr.owner.mail) + + reported_by = "\nReported by {0} <{1}>".format(flask.g.user.name, + flask.g.user.mail) + + try: + msg = MIMEText( + form.comment.data + navigate_to + contact + reported_by, "plain") + except UnicodeEncodeError: + msg = MIMEText(form.comment.data.encode( + "utf-8") + navigate_to + contact + reported_by, "plain", "utf-8") + + msg["Subject"] = "Legal flag raised on {0}".format(coprname) + msg["From"] = "root@{0}".format(hostname) + msg["To"] = ", ".join(send_to) + s = smtplib.SMTP("localhost") + s.sendmail("root@{0}".format(hostname), send_to, msg.as_string()) + s.quit() + + flask.flash("Admin was noticed about your report" + " and will investigate the project shortly.") + + return flask.redirect(flask.url_for("coprs_ns.copr_detail", + username=username, + coprname=coprname)) + + +@coprs_ns.route("///repo//") +def generate_repo_file(username, coprname, chroot): + """ Generate repo file for a given repo name. + Reponame = username-coprname """ + # This solution is used because flask splits off the last part after a + # dash, therefore user-re-po resolves to user-re/po instead of user/re-po + # FAS usernames may not contain dashes, so this construction is safe. + + reponame = "{0}-{1}".format(username, coprname) + + if "-" not in reponame: + return page_not_found( + "Bad repository name: {0}. Must be username-projectname" + .format(reponame)) + + copr = None + try: + # query.one() is used since it fetches all builds, unlike + # query.first(). + copr = coprs_logic.CoprsLogic.get(flask.g.user, username, coprname, + with_builds=True).one() + except sqlalchemy.orm.exc.NoResultFound: + return page_not_found( + "Project {0}/{1} does not exist".format(username, coprname)) + + try: + mock_chroot = coprs_logic.MockChrootsLogic.get_from_name(chroot).one() + except sqlalchemy.orm.exc.NoResultFound: + return page_not_found("Chroot {0} does not exist".format(chroot)) + except ValueError as e: + return page_not_found(str(e)) + + url = "" + for build in copr.builds: + if build.results: + url = build.results + break + + if not url: + return page_not_found( + "Repository not initialized: No finished builds in {0}/{1}." + .format(username, coprname)) + + response = flask.make_response(render_repo(copr, mock_chroot, url)) + response.mimetype = "text/plain" + response.headers["Content-Disposition"] = "filename={0}.repo".format( + reponame) + + return response + + +@coprs_ns.route("///monitor/") +def copr_build_monitor(username, coprname): + query = coprs_logic.CoprsLogic.get( + flask.g.user, username, coprname, with_mock_chroots=True) + form = forms.CoprLegalFlagForm() + try: + copr = query.one() + except sqlalchemy.orm.exc.NoResultFound: + return page_not_found( + "Copr with name {0} does not exist.".format(coprname)) + + builds_query = builds_logic.BuildsLogic.get_multiple( + flask.g.user, copr=copr) + builds = builds_query.order_by("-id").all() + + # please don"t waste time trying to decipher this + # the only reason why this is necessary is non-existent + # database design + # + # loop goes through builds trying to approximate + # per-package results based on previous builds + # - it can"t determine build results if build contains + # more than one package as this data is not available + + out = {} + build = None + chroots = set([chroot.name for chroot in copr.active_chroots]) + latest_build = None + + if builds: + latest_build = builds[0] + chroots.union([chroot.name for chroot in latest_build.build_chroots]) + + chroots = sorted(chroots) + + for build in builds: + chroot_results = {chroot.name: chroot.state + for chroot in build.build_chroots} + + build_results = [] + for chroot_name in chroots: + if chroot_name in chroot_results: + build_results.append((build.id, chroot_results[chroot_name])) + else: + build_results.append((build.id, None)) + + for pkg_url in build.pkgs.split(): + pkg = os.path.basename(pkg_url) + pkg_name = parse_package_name(pkg) + + if pkg_name in out: + continue + + out[pkg_name] = build_results + + return flask.render_template("coprs/detail/monitor.html", + copr=copr, + build=latest_build, + chroots=chroots, + packages=sorted(out.iteritems()), + form=form) diff --git a/frontend/coprs_frontend/coprs/views/misc.py b/frontend/coprs_frontend/coprs/views/misc.py new file mode 100644 index 0000000..4652ab4 --- /dev/null +++ b/frontend/coprs_frontend/coprs/views/misc.py @@ -0,0 +1,153 @@ +import base64 +import datetime +import functools + +import flask + +from coprs import app +from coprs import db +from coprs import helpers +from coprs import models +from coprs import oid + + +@app.before_request +def lookup_current_user(): + flask.g.user = None + if "openid" in flask.session: + flask.g.user = models.User.query.filter( + models.User.openid_name == flask.session["openid"]).first() + + +@app.errorhandler(404) +def page_not_found(message): + return flask.render_template("404.html", message=message), 404 + + +misc = flask.Blueprint("misc", __name__) + + +@misc.route("/login/", methods=["GET"]) +@oid.loginhandler +def login(): + if flask.g.user is not None: + return flask.redirect(oid.get_next_url()) + else: + return oid.try_login("https://id.fedoraproject.org/", + ask_for=["email", "timezone"]) + + +@oid.after_login +def create_or_login(resp): + flask.session["openid"] = resp.identity_url + fasusername = resp.identity_url.replace( + ".id.fedoraproject.org/", "").replace("http://", "") + + # kidding me.. or not + if fasusername and ((app.config["USE_ALLOWED_USERS"] + and fasusername in app.config["ALLOWED_USERS"]) + or not app.config["USE_ALLOWED_USERS"]): + + user = models.User.query.filter( + models.User.openid_name == resp.identity_url).first() + if not user: # create if not created already + expiration_date_token = datetime.date.today() + \ + datetime.timedelta( + days=flask.current_app.config["API_TOKEN_EXPIRATION"]) + + copr64 = base64.b64encode("copr") + "##" + user = models.User(openid_name=resp.identity_url, mail=resp.email, + timezone=resp.timezone, + api_login=copr64 + helpers.generate_api_token( + app.config["API_TOKEN_LENGTH"] - len(copr64)), + api_token=helpers.generate_api_token( + app.config["API_TOKEN_LENGTH"]), + api_token_expiration=expiration_date_token) + else: + user.mail = resp.email + user.timezone = resp.timezone + + db.session.add(user) + db.session.commit() + flask.flash(u"Welcome, {0}".format(user.name)) + flask.g.user = user + + if flask.request.url_root == oid.get_next_url(): + return flask.redirect(flask.url_for("coprs_ns.coprs_by_owner", + username=user.name)) + return flask.redirect(oid.get_next_url()) + else: + flask.flash("User '{0}' is not allowed".format(user.name)) + return flask.redirect(oid.get_next_url()) + + +@misc.route("/logout/") +def logout(): + flask.session.pop("openid", None) + flask.flash(u"You were signed out") + return flask.redirect(oid.get_next_url()) + + +def api_login_required(f): + @functools.wraps(f) + def decorated_function(*args, **kwargs): + token = None + username = None + if "Authorization" in flask.request.headers: + base64string = flask.request.headers["Authorization"] + base64string = base64string.split()[1].strip() + userstring = base64.b64decode(base64string) + (username, token) = userstring.split(":") + token_auth = False + if token and username: + user = models.User.query.filter( + models.User.api_login == username).first() + if (user and user.api_token == token and + user.api_token_expiration >= datetime.date.today()): + + token_auth = True + flask.g.user = user + if not token_auth: + output = {"output": "notok", "error": "Login invalid/expired"} + jsonout = flask.jsonify(output) + jsonout.status_code = 500 + return jsonout + return f(*args, **kwargs) + return decorated_function + + +def login_required(role=helpers.RoleEnum("user")): + def view_wrapper(f): + @functools.wraps(f) + def decorated_function(*args, **kwargs): + if flask.g.user is None: + return flask.redirect(flask.url_for("misc.login", + next=flask.request.url)) + + if role == helpers.RoleEnum("admin") and not flask.g.user.admin: + flask.flash("You are not allowed to access admin section.") + return flask.redirect(flask.url_for("coprs_ns.coprs_show")) + + return f(*args, **kwargs) + return decorated_function + # hack: if login_required is used without params, the "role" parameter + # is in fact the decorated function, so we need to return + # the wrapped function, not the wrapper + # proper solution would be to use login_required() with parentheses + # everywhere, even if they"re empty - TODO + if callable(role): + return view_wrapper(role) + else: + return view_wrapper + + +# backend authentication +def backend_authenticated(f): + @functools.wraps(f) + def decorated_function(*args, **kwargs): + auth = flask.request.authorization + if not auth or auth.password != app.config["BACKEND_PASSWORD"]: + return "You have to provide the correct password", 401 + + return f(*args, **kwargs) + return decorated_function diff --git a/frontend/coprs_frontend/coprs/whoosheers.py b/frontend/coprs_frontend/coprs/whoosheers.py new file mode 100644 index 0000000..e4143d9 --- /dev/null +++ b/frontend/coprs_frontend/coprs/whoosheers.py @@ -0,0 +1,52 @@ +import whoosh + +from flask.ext.whooshee import AbstractWhoosheer + +from coprs import models +from coprs import whooshee + + +@whooshee.register_whoosheer +class CoprUserWhoosheer(AbstractWhoosheer): + schema = whoosh.fields.Schema( + copr_id=whoosh.fields.NUMERIC(stored=True, unique=True), + user_id=whoosh.fields.NUMERIC(stored=True), + username=whoosh.fields.TEXT(), + coprname=whoosh.fields.TEXT(), + description=whoosh.fields.TEXT(), + instructions=whoosh.fields.TEXT()) + + models = [models.Copr, models.User] + + @classmethod + def update_user(cls, writer, user): + # TODO: this is not needed now, as users can't change names, but may be + # needed later + pass + + @classmethod + def update_copr(cls, writer, copr): + writer.update_document(copr_id=copr.id, + user_id=copr.owner.id, + username=copr.owner.name, + coprname=copr.name, + description=copr.description, + instructions=copr.instructions) + + @classmethod + def insert_user(cls, writer, user): + # nothing, user doesn't have coprs yet + pass + + @classmethod + def insert_copr(cls, writer, copr): + writer.add_document(copr_id=copr.id, + user_id=copr.owner.id, + username=copr.owner.name, + coprname=copr.name, + description=copr.description, + instructions=copr.instructions) + + @classmethod + def delete_copr(cls, writer, copr): + writer.delete_by_term("copr_id", copr.id) diff --git a/frontend/coprs_frontend/manage.py b/frontend/coprs_frontend/manage.py new file mode 100755 index 0000000..3822d24 --- /dev/null +++ b/frontend/coprs_frontend/manage.py @@ -0,0 +1,232 @@ +#!/usr/bin/env python + +import argparse +import os +import subprocess + +import flask +from flask.ext.script import Manager, Command, Option, Group + +from coprs import app +from coprs import db +from coprs import exceptions +from coprs import models +from coprs.logic import coprs_logic + + +class TestCommand(Command): + + def run(self, test_args): + os.environ["COPRS_ENVIRON_UNITTEST"] = "1" + if not (("COPR_CONFIG" in os.environ) and os.environ["COPR_CONFIG"]): + os.environ["COPR_CONFIG"] = "/etc/copr/copr_unit_test.conf" + os.environ["PYTHONPATH"] = "." + return subprocess.call(["py.test"] + (test_args or [])) + + option_list = ( + Option("-a", + dest="test_args", + nargs=argparse.REMAINDER), + ) + + +class CreateSqliteFileCommand(Command): + + """ + Create the sqlite DB file (not the tables). + Used for alembic, "create_db" does this automatically. + """ + + def run(self): + if flask.current_app.config["SQLALCHEMY_DATABASE_URI"].startswith("sqlite"): + # strip sqlite:/// + datadir_name = os.path.dirname( + flask.current_app.config["SQLALCHEMY_DATABASE_URI"][10:]) + if not os.path.exists(datadir_name): + os.makedirs(datadir_name) + + +class CreateDBCommand(Command): + + """ + Create the DB schema + """ + + def run(self, alembic_ini=None): + CreateSqliteFileCommand().run() + db.create_all() + + # load the Alembic configuration and generate the + # version table, "stamping" it with the most recent rev: + from alembic.config import Config + from alembic import command + alembic_cfg = Config(alembic_ini) + command.stamp(alembic_cfg, "head") + + option_list = ( + Option("--alembic", + "-f", + dest="alembic_ini", + help="Path to the alembic configuration file (alembic.ini)", + required=True), + ) + + +class DropDBCommand(Command): + + """ + Delete DB + """ + + def run(self): + db.drop_all() + + +class ChrootCommand(Command): + + def print_invalid_format(self, chroot_name): + print( + "{0} - invalid chroot format, must be '{release}-{version}-{arch}'." + .format(chroot_name)) + + def print_already_exists(self, chroot_name): + print("{0} - already exists.".format(chroot_name)) + + def print_doesnt_exist(self, chroot_name): + print("{0} - chroot doesn\"t exist.".format(chroot_name)) + + option_list = ( + Option("chroot_names", + help="Chroot name, e.g. fedora-18-x86_64.", + nargs="+"), + ) + + +class CreateChrootCommand(ChrootCommand): + + "Creates a mock chroot in DB" + + def run(self, chroot_names): + for chroot_name in chroot_names: + try: + coprs_logic.MockChrootsLogic.add(None, chroot_name) + db.session.commit() + except exceptions.MalformedArgumentException: + self.print_invalid_format(chroot_name) + except exceptions.DuplicateException: + self.print_already_exists(chroot_name) + + +class AlterChrootCommand(ChrootCommand): + + "Activates or deactivates a chroot" + + def run(self, chroot_names, action): + activate = (action == "activate") + for chroot_name in chroot_names: + try: + coprs_logic.MockChrootsLogic.edit_by_name( + None, chroot_name, activate) + db.session.commit() + except exceptions.MalformedArgumentException: + self.print_invalid_format(chroot_name) + except exceptions.NotFoundException: + self.print_doesnt_exist(chroot_name) + + option_list = ChrootCommand.option_list + ( + Option("--action", + "-a", + dest="action", + help="Action to take - currently activate or deactivate", + choices=["activate", "deactivate"], + required=True), + ) + + +class DropChrootCommand(ChrootCommand): + + "Activates or deactivates a chroot" + + def run(self, chroot_names): + for chroot_name in chroot_names: + try: + coprs_logic.MockChrootsLogic.delete_by_name(None, chroot_name) + db.session.commit() + except exceptions.MalformedArgumentException: + self.print_invalid_format(chroot_name) + except exceptions.NotFoundException: + self.print_doesnt_exist(chroot_name) + + +class DisplayChrootsCommand(Command): + + "Displays current mock chroots" + + def run(self, active_only): + for ch in coprs_logic.MockChrootsLogic.get_multiple( + None, active_only=active_only).all(): + + print(ch.name) + + option_list = ( + Option("--active-only", + "-a", + dest="active_only", + help="Display only active chroots", + required=False, + action="store_true", + default=False), + ) + + +class AlterUserCommand(Command): + + def run(self, name, **kwargs): + user = models.User.query.filter( + models.User.openid_name == models.User.openidize_name(name)).first() + if not user: + print("No user named {0}.".format(name)) + return + + if kwargs["admin"]: + user.admin = True + if kwargs["no_admin"]: + user.admin = False + if kwargs["proven"]: + user.proven = True + if kwargs["no_proven"]: + user.proven = False + + db.session.commit() + + option_list = ( + Option("name"), + Group( + Option("--admin", + action="store_true"), + Option("--no-admin", + action="store_true"), + exclusive=True + ), + Group( + Option("--proven", + action="store_true"), + Option("--no-proven", + action="store_true"), + exclusive=True + ) + ) + +manager = Manager(app) +manager.add_command("test", TestCommand()) +manager.add_command("create_sqlite_file", CreateSqliteFileCommand()) +manager.add_command("create_db", CreateDBCommand()) +manager.add_command("drop_db", DropDBCommand()) +manager.add_command("create_chroot", CreateChrootCommand()) +manager.add_command("alter_chroot", AlterChrootCommand()) +manager.add_command("display_chroots", DisplayChrootsCommand()) +manager.add_command("drop_chroot", DropChrootCommand()) +manager.add_command("alter_user", AlterUserCommand()) + +if __name__ == "__main__": + manager.run() diff --git a/frontend/coprs_frontend/tests/__init__.py b/frontend/coprs_frontend/tests/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/frontend/coprs_frontend/tests/__init__.py diff --git a/frontend/coprs_frontend/tests/coprs_test_case.py b/frontend/coprs_frontend/tests/coprs_test_case.py new file mode 100644 index 0000000..313cef7 --- /dev/null +++ b/frontend/coprs_frontend/tests/coprs_test_case.py @@ -0,0 +1,224 @@ +import base64 +import os +import time +from functools import wraps + +import pytest +import decorator + +import coprs + +from coprs import helpers +from coprs import models + + +class CoprsTestCase(object): + + def setup_method(self, method): + self.tc = coprs.app.test_client() + self.app = coprs.app + self.app.testing = True + self.db = coprs.db + self.db.session = self.db.create_scoped_session() + self.models = models + self.helpers = helpers + self.backend_passwd = coprs.app.config["BACKEND_PASSWORD"] + # create datadir if it doesn't exist + datadir = os.path.commonprefix( + [self.app.config["DATABASE"], self.app.config["OPENID_STORE"]]) + if not os.path.exists(datadir): + os.makedirs(datadir) + coprs.db.create_all() + self.db.session.commit() + + def teardown_method(self, method): + # delete just data, not the tables + for tbl in reversed(self.db.metadata.sorted_tables): + self.db.engine.execute(tbl.delete()) + + @property + def auth_header(self): + return {"Authorization": "Basic " + + base64.b64encode("doesntmatter:{0}".format(self.backend_passwd))} + + @pytest.fixture + def f_db(self): + self.db.session.commit() + + @pytest.fixture + def f_users(self): + self.u1 = models.User( + openid_name=u"http://user1.id.fedoraproject.org/", + proven=False, + admin=True, + mail="user1@foo.bar") + + self.u2 = models.User( + openid_name=u"http://user2.id.fedoraproject.org/", + proven=False, + mail="user2@spam.foo") + + self.u3 = models.User( + openid_name=u"http://user3.id.fedoraproject.org/", + proven=False, + mail="baz@bar.bar") + + self.db.session.add_all([self.u1, self.u2, self.u3]) + + @pytest.fixture + def f_coprs(self): + self.c1 = models.Copr(name=u"foocopr", owner=self.u1) + self.c2 = models.Copr(name=u"foocopr", owner=self.u2) + self.c3 = models.Copr(name=u"barcopr", owner=self.u2) + + self.db.session.add_all([self.c1, self.c2, self.c3]) + + @pytest.fixture + def f_mock_chroots(self): + self.mc1 = models.MockChroot( + os_release="fedora", os_version="18", arch="x86_64", is_active=True) + self.mc2 = models.MockChroot( + os_release="fedora", os_version="17", arch="x86_64", is_active=True) + self.mc3 = models.MockChroot( + os_release="fedora", os_version="17", arch="i386", is_active=True) + self.mc4 = models.MockChroot( + os_release="fedora", os_version="rawhide", arch="i386", is_active=True) + + # only bind to coprs if the test has used the f_coprs fixture + if hasattr(self, "c1"): + cc1 = models.CoprChroot() + cc1.mock_chroot = self.mc1 + # c1 foocopr with fedora-18-x86_64 + self.c1.copr_chroots.append(cc1) + + cc2 = models.CoprChroot() + cc2.mock_chroot = self.mc2 + cc3 = models.CoprChroot() + cc3.mock_chroot = self.mc3 + # c2 foocopr with fedora-17-i386 fedora-17-x86_64 + self.c2.copr_chroots.append(cc2) + self.c2.copr_chroots.append(cc3) + + cc4 = models.CoprChroot() + cc4.mock_chroot = self.mc4 + # c3 barcopr with fedora-rawhide-i386 + self.c3.copr_chroots.append(cc4) + self.db.session.add_all([cc1, cc2, cc3, cc4]) + + self.db.session.add_all([self.mc1, self.mc2, self.mc3, self.mc4]) + + @pytest.fixture + def f_builds(self): + self.b1 = models.Build( + copr=self.c1, user=self.u1, submitted_on=50, started_on=139086644000) + self.b2 = models.Build( + copr=self.c1, user=self.u2, submitted_on=10, ended_on=139086644000) + self.b3 = models.Build( + copr=self.c2, user=self.u2, submitted_on=10) + self.b4 = models.Build( + copr=self.c2, user=self.u2, submitted_on=100) + + for build in [self.b1, self.b2, self.b3, self.b4]: + self.db.session.add(build) + + for chroot in build.copr.active_chroots: + buildchroot = models.BuildChroot( + build=build, + mock_chroot=chroot) + + self.db.session.add(buildchroot) + + self.db.session.add_all([self.b1, self.b2, self.b3, self.b4]) + + @pytest.fixture + def f_copr_permissions(self): + self.cp1 = models.CoprPermission( + copr=self.c2, + user=self.u1, + copr_builder=helpers.PermissionEnum("approved"), + copr_admin=helpers.PermissionEnum("nothing")) + + self.cp2 = models.CoprPermission( + copr=self.c3, + user=self.u3, + copr_builder=helpers.PermissionEnum("nothing"), + copr_admin=helpers.PermissionEnum("nothing")) + + self.cp3 = models.CoprPermission( + copr=self.c3, + user=self.u1, + copr_builder=helpers.PermissionEnum("request"), + copr_admin=helpers.PermissionEnum("approved")) + + self.db.session.add_all([self.cp1, self.cp2, self.cp3]) + + @pytest.fixture + def f_actions(self): + # if using actions, we need to flush coprs into db, so that we can get + # their ids + self.f_db() + self.a1 = models.Action(action_type=helpers.ActionTypeEnum("rename"), + object_type="copr", + object_id=self.c1.id, + old_value="{0}/{1}".format( + self.c1.owner.name, self.c1.name), + new_value="{0}/new_name".format( + self.c1.owner.name), + created_on=int(time.time())) + self.a2 = models.Action(action_type=helpers.ActionTypeEnum("rename"), + object_type="copr", + object_id=self.c2.id, + old_value="{0}/{1}".format( + self.c2.owner.name, self.c2.name), + new_value="{0}/new_name2".format( + self.c2.owner.name), + created_on=int(time.time())) + self.a3 = models.Action(action_type=helpers.ActionTypeEnum("delete"), + object_type="copr", + object_id=100, + old_value="asd/qwe", + new_value=None, + result=helpers.BackendResultEnum("success"), + created_on=int(time.time())) + self.db.session.add_all([self.a1, self.a2, self.a3]) + + +class TransactionDecorator(object): + + """ + This is decorator as a class. + + Its purpose is to replace repetative lines of 'with' statements + in test's functions. Everytime you find your self writing test function + which uses following 'with's construct: + + with self.tc as test_client: + with c.session_transaction() as session: + session['openid'] = self.u.openid_name + + where 'u' stands for any user from 'f_users' fixture, use this to decorate + your test function: + + @TransactionDecorator('u') + def test_function_without_with_statements(self, f_users): + # write code as you were in with 'self.tc as test_client' indent + # you can also access object 'test_client' through 'self.test_client' + + where decorator parameter ''u'' stands for string representation of any + user from 'f_users' fixture from which you wish to store 'openid_name'. + Please note that you **must** include 'f_users' fixture in decorated + function parameters. + + """ + + def __init__(self, user): + self.user = user + + def __call__(self, fn): + @wraps(fn) + def wrapper(fn, fn_self, *args): + with fn_self.tc as fn_self.test_client: + with fn_self.test_client.session_transaction() as session: + session["openid"] = getattr(fn_self, self.user).openid_name + return fn(fn_self, *args) + return decorator.decorator(wrapper, fn) diff --git a/frontend/coprs_frontend/tests/test_helpers.py b/frontend/coprs_frontend/tests/test_helpers.py new file mode 100644 index 0000000..6851bf6 --- /dev/null +++ b/frontend/coprs_frontend/tests/test_helpers.py @@ -0,0 +1,27 @@ +from coprs.helpers import parse_package_name + +from tests.coprs_test_case import CoprsTestCase + + +class TestHelpers(CoprsTestCase): + + def test_guess_package_name(self): + EXP = { + 'wat-1.2.rpm': 'wat', + 'will-crash-0.5-2.fc20.src.rpm': 'will-crash', + 'will-crash-0.5-2.fc20.src': 'will-crash', + 'will-crash-0.5-2.fc20': 'will-crash', + 'will-crash-0.5-2': 'will-crash', + 'will-crash-0.5-2.rpm': 'will-crash', + 'will-crash-0.5-2.src.rpm': 'will-crash', + 'will-crash': 'will-crash', + 'pkgname7.src.rpm': 'pkgname7', + 'copr-frontend-1.14-1.git.65.9ba5393.fc20.noarch': 'copr-frontend', + 'noversion.fc20.src.rpm': 'noversion', + 'nothing': 'nothing', + 'ruby193': 'ruby193', + 'xorg-x11-fonts-ISO8859-1-75dpi-7.1-2.1.el5.noarch.rpm': 'xorg-x11-fonts-ISO8859-1-75dpi', + } + + for pkg, expected in EXP.iteritems(): + assert parse_package_name(pkg) == expected diff --git a/frontend/coprs_frontend/tests/test_logic/test_builds_logic.py b/frontend/coprs_frontend/tests/test_logic/test_builds_logic.py new file mode 100644 index 0000000..9871664 --- /dev/null +++ b/frontend/coprs_frontend/tests/test_logic/test_builds_logic.py @@ -0,0 +1,40 @@ +import pytest + +from coprs.exceptions import ActionInProgressException +from coprs.logic.builds_logic import BuildsLogic + +from tests.coprs_test_case import CoprsTestCase + + +class TestBuildsLogic(CoprsTestCase): + + def test_add_only_adds_active_chroots(self, f_users, f_coprs, f_builds, + f_mock_chroots, f_db): + + self.mc2.is_active = False + self.db.session.commit() + b = BuildsLogic.add(self.u2, "blah blah", self.c2) + self.db.session.commit() + assert b.chroots[0].name == self.mc3.name + + def test_add_raises_if_copr_has_unfinished_actions(self, f_users, f_coprs, + f_actions, f_db): + + with pytest.raises(ActionInProgressException): + b = BuildsLogic.add(self.u1, "blah blah", self.c1) + self.db.session.rollback() + + def test_add_assigns_params_correctly(self, f_users, f_coprs, + f_mock_chroots, f_db): + + params = dict( + user=self.u1, + pkgs="blah blah", + copr=self.c1, + repos="repos", + memory_reqs=3000, + timeout=5000) + + b = BuildsLogic.add(**params) + for k, v in params.items(): + assert getattr(b, k) == v diff --git a/frontend/coprs_frontend/tests/test_logic/test_coprs_logic.py b/frontend/coprs_frontend/tests/test_logic/test_coprs_logic.py new file mode 100644 index 0000000..39f600a --- /dev/null +++ b/frontend/coprs_frontend/tests/test_logic/test_coprs_logic.py @@ -0,0 +1,30 @@ +import pytest + +from coprs.exceptions import ActionInProgressException +from coprs.helpers import ActionTypeEnum +from coprs.logic.coprs_logic import CoprsLogic + +from tests.coprs_test_case import CoprsTestCase + + +class TestCoprsLogic(CoprsTestCase): + + def test_update_raises_if_copr_has_unfinished_actions(self, f_users, + f_coprs, f_actions, + f_db): + self.c1.name = "foo" + with pytest.raises(ActionInProgressException): + CoprsLogic.update(self.u1, self.c1) + self.db.session.rollback() + + def test_legal_flag_doesnt_block_copr_functionality(self, f_users, + f_coprs, f_db): + self.db.session.add(self.models.Action( + object_type="copr", + object_id=self.c1.id, + action_type=ActionTypeEnum("legal-flag"))) + + self.db.session.commit() + # test will fail if this raises exception + CoprsLogic.raise_if_unfinished_blocking_action( + None, self.c1, "ha, failed") diff --git a/frontend/coprs_frontend/tests/test_views/test_admin/test_admin_general.py b/frontend/coprs_frontend/tests/test_views/test_admin/test_admin_general.py new file mode 100644 index 0000000..60eba07 --- /dev/null +++ b/frontend/coprs_frontend/tests/test_views/test_admin/test_admin_general.py @@ -0,0 +1,23 @@ +from tests.coprs_test_case import CoprsTestCase + + +class TestAdminLogin(CoprsTestCase): + # TODO: test on something better then page title - maybe see rendered + # templates? + text_to_check = "Coprs - Admin" + + def test_nonadmin_cant_login(self, f_users, f_db): + with self.tc as c: + with c.session_transaction() as s: + s["openid"] = self.u2.openid_name + + r = c.get("/admin/", follow_redirects=True) + assert self.text_to_check not in r.data + + def test_admin_can_login(self, f_users, f_db): + with self.tc as c: + with c.session_transaction() as s: + s["openid"] = self.u1.openid_name + + r = c.get("/admin/", follow_redirects=True) + assert self.text_to_check in r.data diff --git a/frontend/coprs_frontend/tests/test_views/test_backend_ns/test_backend_general.py b/frontend/coprs_frontend/tests/test_views/test_backend_ns/test_backend_general.py new file mode 100644 index 0000000..c46d5b3 --- /dev/null +++ b/frontend/coprs_frontend/tests/test_views/test_backend_ns/test_backend_general.py @@ -0,0 +1,242 @@ +import json + +from coprs.signals import build_finished +from tests.coprs_test_case import CoprsTestCase + + +class TestWaitingBuilds(CoprsTestCase): + + def test_no_waiting_builds(self): + assert '"builds": []' in self.tc.get( + "/backend/waiting/", headers=self.auth_header).data + + def test_waiting_build_only_lists_not_started_or_ended( + self, f_users, f_coprs, f_mock_chroots, f_builds, f_db): + + r = self.tc.get("/backend/waiting/", headers=self.auth_header) + assert len(json.loads(r.data)["builds"]) == 4 + + +# status = 0 # failure +# status = 1 # succeeded +class TestUpdateBuilds(CoprsTestCase): + data1 = """ +{ + "builds":[ + { + "id": 1, + "copr_id": 2, + "results": "http://server/results/foo/bar/", + "started_on": 139086644000 + } + ] +}""" + + data2 = """ +{ + "builds":[ + { + "id": 1, + "copr_id": 2, + "status": 1, + "chroot": "fedora-18-x86_64", + "ended_on": 139086644000 + } + ] +}""" + + data3 = """ +{ + "builds":[ + { + "id": 1, + "copr_id": 2, + "started_on": 139086644000 + }, + { + "id": 2, + "copr_id": 1, + "status": 0, + "chroot": "fedora-18-x86_64", + "results": "http://server/results/foo/bar/", + "ended_on": 139086644000 + }, + { + "id": 123321, + "copr_id": 1, + "status": 0, + "ended_on": 139086644000 + }, + { + "id": 1234321, + "copr_id": 2, + "results": "http://server/results/foo/bar/", + "started_on": 139086644000 + } + ] +}""" + + def test_updating_requires_password(self, f_users, f_coprs, f_builds, f_db): + r = self.tc.post("/backend/update/", + content_type="application/json", + data="") + assert "You have to provide the correct password" in r.data + + def test_update_build_started(self, f_users, f_coprs, f_builds, f_db): + self.b1.started_on = None + self.db.session.add(self.b1) + self.db.session.commit() + + r = self.tc.post("/backend/update/", + content_type="application/json", + headers=self.auth_header, + data=self.data1) + assert json.loads(r.data)["updated_builds_ids"] == [1] + assert json.loads(r.data)["non_existing_builds_ids"] == [] + + updated = self.models.Build.query.filter( + self.models.Build.id == 1).first() + assert updated.results == "http://server/results/foo/bar/" + assert updated.started_on == 139086644000 + + def test_update_build_ended(self, f_users, f_coprs, f_mock_chroots, + f_builds, f_db): + + r = self.tc.post("/backend/update/", + content_type="application/json", + headers=self.auth_header, + data=self.data2) + assert json.loads(r.data)["updated_builds_ids"] == [1] + assert json.loads(r.data)["non_existing_builds_ids"] == [] + + updated = self.models.Build.query.filter( + self.models.Build.id == 1).first() + assert updated.status == 1 + assert updated.ended_on == 139086644000 + + def test_update_more_existent_and_non_existent_builds( + self, f_users, f_coprs, f_mock_chroots, f_builds, f_db): + + self.b1.started_on = None + self.db.session.add(self.b1) + self.db.session.commit() + + r = self.tc.post("/backend/update/", + content_type="application/json", + headers=self.auth_header, + data=self.data3) + + assert sorted(json.loads(r.data)["updated_builds_ids"]) == [1, 2] + assert sorted(json.loads(r.data)["non_existing_builds_ids"]) == [ + 123321, 1234321] + + started = self.models.Build.query.filter( + self.models.Build.id == 1).first() + assert started.started_on == 139086644000 + + ended = self.models.Build.query.filter( + self.models.Build.id == 2).first() + assert ended.status == 0 + assert ended.results == "http://server/results/foo/bar/" + assert ended.ended_on == 139086644000 + + def test_build_ended_emmits_signal(self, f_users, f_coprs, f_builds, f_db): + # TODO: this should probably be mocked... + signals_received = [] + + def test_receiver(sender, **kwargs): + signals_received.append(kwargs["build"]) + build_finished.connect(test_receiver) + self.tc.post("/backend/update/", + content_type="application/json", + headers=self.auth_header, + data=self.data3) + assert len(signals_received) == 1 + self.db.session.add(self.b2) + assert signals_received[0].id == 2 + + +class TestWaitingActions(CoprsTestCase): + + def test_no_waiting_actions(self): + assert '"actions": []' in self.tc.get( + "/backend/waiting/", headers=self.auth_header).data + + def test_waiting_actions_only_lists_not_started_or_ended( + self, f_users, f_coprs, f_actions, f_db): + + r = self.tc.get("/backend/waiting/", headers=self.auth_header) + assert len(json.loads(r.data)["actions"]) == 2 + + +class TestUpdateActions(CoprsTestCase): + data1 = """ +{ + "actions":[ + { + "id": 1, + "result": 1, + "message": "no problem", + "ended_on": 139086644000 + } + ] +}""" + data2 = """ +{ + "actions":[ + { + "id": 1, + "result": 1, + "message": null, + "ended_on": 139086644000 + }, + { + "id": 2, + "result": 2, + "message": "problem!", + "ended_on": 139086644000 + }, + { + "id": 100, + "result": 123, + "message": "wheeeee!", + "ended_on": 139086644000 + } + ] +}""" + + def test_update_one_action(self, f_users, f_coprs, f_actions, f_db): + r = self.tc.post("/backend/update/", + content_type="application/json", + headers=self.auth_header, + data=self.data1) + assert json.loads(r.data)["updated_actions_ids"] == [1] + assert json.loads(r.data)["non_existing_actions_ids"] == [] + + updated = self.models.Action.query.filter( + self.models.Action.id == 1).first() + assert updated.result == 1 + assert updated.message == "no problem" + assert updated.ended_on == 139086644000 + + def test_update_more_existent_and_non_existent_builds(self, f_users, + f_coprs, f_actions, + f_db): + r = self.tc.post("/backend/update/", + content_type="application/json", + headers=self.auth_header, + data=self.data2) + assert sorted(json.loads(r.data)["updated_actions_ids"]) == [1, 2] + assert json.loads(r.data)["non_existing_actions_ids"] == [100] + + updated = self.models.Action.query.filter( + self.models.Action.id == 1).first() + assert updated.result == 1 + assert updated.message is None + assert updated.ended_on == 139086644000 + + updated2 = self.models.Action.query.filter( + self.models.Action.id == 2).first() + assert updated2.result == 2 + assert updated2.message == "problem!" + assert updated2.ended_on == 139086644000 diff --git a/frontend/coprs_frontend/tests/test_views/test_coprs_ns/__init__.py b/frontend/coprs_frontend/tests/test_views/test_coprs_ns/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/frontend/coprs_frontend/tests/test_views/test_coprs_ns/__init__.py diff --git a/frontend/coprs_frontend/tests/test_views/test_coprs_ns/test_coprs_builds.py b/frontend/coprs_frontend/tests/test_views/test_coprs_ns/test_coprs_builds.py new file mode 100644 index 0000000..c88799c --- /dev/null +++ b/frontend/coprs_frontend/tests/test_views/test_coprs_ns/test_coprs_builds.py @@ -0,0 +1,181 @@ +from tests.coprs_test_case import CoprsTestCase, TransactionDecorator + + +class TestCoprShowBuilds(CoprsTestCase): + + def test_copr_show_builds(self, f_users, f_coprs, f_mock_chroots, + f_builds, f_db): + + r = self.tc.get( + "/coprs/{0}/{1}/builds/".format(self.u2.name, self.c2.name)) + assert r.data.count('') == 3 + + +class TestCoprsOwned(CoprsTestCase): + + @TransactionDecorator("u3") + def test_owned_none(self, f_users, f_coprs, f_db): + self.db.session.add(self.u3) + r = self.test_client.get("/coprs/{0}/".format(self.u3.name)) + assert "No projects..." in r.data + + @TransactionDecorator("u1") + def test_owned_one(self, f_users, f_coprs, f_db): + self.db.session.add(self.u1) + r = self.test_client.get("/coprs/{0}/".format(self.u1.name)) + assert r.data.count('
') == 1 + + +class TestCoprsAllowed(CoprsTestCase): + + @TransactionDecorator("u3") + def test_allowed_none(self, f_users, f_coprs, f_copr_permissions, f_db): + self.db.session.add(self.u3) + r = self.test_client.get("/coprs/{0}/allowed/".format(self.u3.name)) + assert "No projects..." in r.data + + @TransactionDecorator("u1") + def test_allowed_one(self, f_users, f_coprs, f_copr_permissions, f_db): + self.db.session.add(self.u1) + r = self.test_client.get("/coprs/{0}/allowed/".format(self.u1.name)) + assert r.data.count('
') == 1 + + +class TestCoprNew(CoprsTestCase): + success_string = "New project was successfully created" + + @TransactionDecorator("u1") + def test_copr_new_normal(self, f_users, f_mock_chroots, f_db): + r = self.test_client.post( + "/coprs/{0}/new/".format(self.u1.name), + data={"name": "foo", + "fedora-rawhide-i386": "y", + "arches": ["i386"]}, + follow_redirects=True) + + assert self.models.Copr.query.filter( + self.models.Copr.name == "foo").first() + assert self.success_string in r.data + + # make sure no initial build was submitted + assert self.models.Build.query.first() is None + + @TransactionDecorator("u1") + def test_copr_new_emits_signal(self, f_users, f_mock_chroots, f_db): + # TODO: this should probably be mocked... + signals_received = [] + + def test_receiver(sender, **kwargs): + signals_received.append(kwargs["copr"]) + copr_created.connect(test_receiver) + self.test_client.post( + "/coprs/{0}/new/".format(self.u1.name), + data={"name": "foo", + "fedora-rawhide-i386": "y", + "arches": ["i386"]}, + follow_redirects=True) + + assert len(signals_received) == 1 + assert signals_received[0].name == "foo" + + @TransactionDecorator("u3") + def test_copr_new_exists_for_another_user(self, f_users, f_coprs, + f_mock_chroots, f_db): + + self.db.session.add(self.c1) + foocoprs = len(self.models.Copr.query.filter( + self.models.Copr.name == self.c1.name).all()) + assert foocoprs > 0 + + r = self.test_client.post( + "/coprs/{0}/new/".format(self.u3.name), + data={"name": self.c1.name, + "fedora-rawhide-i386": "y"}, + follow_redirects=True) + + self.db.session.add(self.c1) + + assert len(self.models.Copr.query.filter( + self.models.Copr.name == self.c1.name).all()) == foocoprs + 1 + assert self.success_string in r.data + + @TransactionDecorator("u1") + def test_copr_new_exists_for_this_user(self, f_users, f_coprs, + f_mock_chroots, f_db): + self.db.session.add(self.c1) + foocoprs = len(self.models.Copr.query.filter( + self.models.Copr.name == self.c1.name).all()) + assert foocoprs > 0 + + r = self.test_client.post( + "/coprs/{0}/new/".format(self.u1.name), + data={"name": self.c1.name, + "fedora-rawhide-i386": "y"}, + follow_redirects=True) + + self.db.session.add(self.c1) + assert len(self.models.Copr.query.filter( + self.models.Copr.name == self.c1.name).all()) == foocoprs + assert "You already have project named" in r.data + + @TransactionDecorator("u1") + def test_copr_new_with_initial_pkgs(self, f_users, f_mock_chroots, f_db): + r = self.test_client.post("/coprs/{0}/new/".format(self.u1.name), + data={"name": "foo", + "fedora-rawhide-i386": "y", + "initial_pkgs": ["http://f", + "http://b"]}, + follow_redirects=True) + + copr = self.models.Copr.query.filter( + self.models.Copr.name == "foo").first() + assert copr + assert self.success_string in r.data + + assert self.models.Build.query.first().copr == copr + assert copr.build_count == 1 + assert "Initial packages were successfully submitted" in r.data + + @TransactionDecorator("u1") + def test_copr_new_is_allowed_even_if_deleted_has_same_name( + self, f_users, f_coprs, f_mock_chroots, f_db): + + self.db.session.add(self.c1) + self.c1.deleted = True + self.c1.owner = self.u1 + self.db.session.commit() + + self.db.session.add(self.c1) + r = self.test_client.post("/coprs/{0}/new/".format(self.u1.name), + data={"name": self.c1.name, + "fedora-rawhide-i386": "y", + "arches": ["i386"]}, + follow_redirects=True) + + self.c1 = self.db.session.merge(self.c1) + self.u1 = self.db.session.merge(self.u1) + assert len(self.models.Copr.query.filter(self.models.Copr.name == + self.c1.name) + .filter(self.models.Copr.owner == self.u1) + .all()) == 2 + assert self.success_string in r.data + + +class TestCoprDetail(CoprsTestCase): + + def test_copr_detail_not_found(self): + r = self.tc.get("/coprs/foo/bar/") + assert r.status_code == 404 + + def test_copr_detail_normal(self, f_users, f_coprs, f_db): + r = self.tc.get("/coprs/{0}/{1}/".format(self.u1.name, self.c1.name)) + assert r.status_code == 200 + assert self.c1.name in r.data + + def test_copr_detail_contains_builds(self, f_users, f_coprs, + f_mock_chroots, f_builds, f_db): + r = self.tc.get( + "/coprs/{0}/{1}/builds/".format(self.u1.name, self.c1.name)) + assert r.data.count('{0}'.format(self.u3.name) in r.data + assert '{0}'.format(self.u1.name) in r.data + + @TransactionDecorator("u1") + def test_copr_detail_allows_asking_for_permissions(self, f_users, f_coprs, + f_copr_permissions, f_db): + + self.db.session.add_all([self.u2, self.c2]) + r = self.test_client.get( + "/coprs/{0}/{1}/permissions/".format(self.u2.name, self.c2.name)) + # u1 is approved builder, check for that + assert "/permissions_applier_change/" in r.data + + @TransactionDecorator("u2") + def test_copr_detail_doesnt_allow_owner_to_ask_for_permissions( + self, f_users, f_coprs, f_db): + + self.db.session.add_all([self.u2, self.c2]) + r = self.test_client.get( + "/coprs/{0}/{1}/permissions/".format(self.u2.name, self.c2.name)) + assert "/permissions_applier_change/" not in r.data + + @TransactionDecorator("u2") + def test_detail_has_correct_permissions_form(self, f_users, f_coprs, + f_copr_permissions, f_db): + + self.db.session.add_all([self.u2, self.c3]) + r = self.test_client.get( + "/coprs/{0}/{1}/permissions/".format(self.u2.name, self.c3.name)) + + assert r.data.count("nothing") == 2 + assert '' in r.data + + def test_copr_detail_doesnt_show_cancel_build_for_anonymous(self, f_users, f_coprs, f_builds, f_db): + r = self.tc.get("/coprs/{0}/{1}/".format(self.u2.name, self.c2.name)) + assert "/cancel_build/" not in r.data + + @TransactionDecorator("u1") + def test_copr_detail_doesnt_allow_non_submitter_to_cancel_build( + self, f_users, f_coprs, f_mock_chroots, f_builds, f_db): + + self.db.session.add_all([self.u2, self.c2]) + r = self.test_client.get( + "/coprs/{0}/{1}/builds/".format(self.u2.name, self.c2.name)) + assert "/cancel_build/" not in r.data + + @TransactionDecorator("u2") + def test_copr_detail_allows_submitter_to_cancel_build( + self, f_users, f_coprs, f_mock_chroots, f_builds, f_db): + + self.db.session.add_all([self.u2, self.c2]) + r = self.test_client.get( + "/coprs/{0}/{1}/builds/".format(self.u2.name, self.c2.name)) + assert "/cancel_build/" in r.data + + +class TestCoprEdit(CoprsTestCase): + + @TransactionDecorator("u1") + def test_edit_prefills_id(self, f_users, f_coprs, f_db): + self.db.session.add_all([self.u1, self.c1]) + r = self.test_client.get( + "/coprs/{0}/{1}/edit/".format(self.u1.name, self.c1.name)) + # TODO: use some kind of html parsing library to look + # for the hidden input, this ties us + # to the precise format of the tag + assert ('' + .format(self.c1.id) in r.data) + + +class TestCoprUpdate(CoprsTestCase): + + @TransactionDecorator("u1") + def test_update_no_changes(self, f_users, f_coprs, f_mock_chroots, f_db): + self.db.session.add_all([self.u1, self.c1]) + r = self.test_client.post("/coprs/{0}/{1}/update/" + .format(self.u1.name, self.c1.name), + data={"name": self.c1.name, + "fedora-18-x86_64": "y", + "id": self.c1.id}, + follow_redirects=True) + + assert "Project was updated successfully" in r.data + + @TransactionDecorator("u1") + def test_copr_admin_can_update(self, f_users, f_coprs, + f_copr_permissions, f_mock_chroots, f_db): + + self.db.session.add_all([self.u2, self.c3]) + r = self.test_client.post("/coprs/{0}/{1}/update/" + .format(self.u2.name, self.c3.name), + data={"name": self.c3.name, + "fedora-rawhide-i386": "y", + "id": self.c3.id}, + follow_redirects=True) + + assert "Project was updated successfully" in r.data + + @TransactionDecorator("u1") + def test_update_multiple_chroots(self, f_users, f_coprs, + f_copr_permissions, f_mock_chroots, f_db): + + self.db.session.add_all( + [self.u1, self.c1, self.mc1, self.mc2, self.mc3]) + r = self.test_client.post("/coprs/{0}/{1}/update/" + .format(self.u1.name, self.c1.name), + data={"name": self.c1.name, + self.mc2.name: "y", + self.mc3.name: "y", + "id": self.c1.id}, + follow_redirects=True) + + assert "Project was updated successfully" in r.data + self.c1 = self.db.session.merge(self.c1) + self.mc1 = self.db.session.merge(self.mc1) + self.mc2 = self.db.session.merge(self.mc2) + self.mc3 = self.db.session.merge(self.mc3) + + mock_chroots = (self.models.MockChroot.query + .join(self.models.CoprChroot) + .filter(self.models.CoprChroot.copr_id == + self.c1.id).all()) + + mock_chroots_names = map(lambda x: x.name, mock_chroots) + assert self.mc2.name in mock_chroots_names + assert self.mc3.name in mock_chroots_names + assert self.mc1.name not in mock_chroots_names + + @TransactionDecorator("u2") + def test_update_deletes_multiple_chroots(self, f_users, f_coprs, + f_copr_permissions, + f_mock_chroots, f_db): + + # https://fedorahosted.org/copr/ticket/42 + self.db.session.add_all([self.u2, self.c2, self.mc1]) + # add one more mock_chroot, so that we can remove two + cc = self.models.CoprChroot() + cc.mock_chroot = self.mc1 + self.c2.copr_chroots.append(cc) + + r = self.test_client.post("/coprs/{0}/{1}/update/" + .format(self.u2.name, self.c2.name), + data={"name": self.c2.name, + self.mc1.name: "y", + "id": self.c2.id}, + follow_redirects=True) + + assert "Project was updated successfully" in r.data + self.c2 = self.db.session.merge(self.c2) + self.mc1 = self.db.session.merge(self.mc1) + mock_chroots = (self.models.MockChroot.query + .join(self.models.CoprChroot) + .filter(self.models.CoprChroot.copr_id == + self.c2.id).all()) + + assert len(mock_chroots) == 1 + + +class TestCoprApplyForPermissions(CoprsTestCase): + + @TransactionDecorator("u2") + def test_apply(self, f_users, f_coprs, f_db): + self.db.session.add_all([self.u1, self.u2, self.c1]) + r = self.test_client.post("/coprs/{0}/{1}/permissions_applier_change/" + .format(self.u1.name, self.c1.name), + data={"copr_builder": "1"}, + follow_redirects=True) + + assert "Successfuly updated" in r.data + + self.u1 = self.db.session.merge(self.u1) + self.u2 = self.db.session.merge(self.u2) + self.c1 = self.db.session.merge(self.c1) + new_perm = (self.models.CoprPermission.query + .filter(self.models.CoprPermission.user_id == self.u2.id) + .filter(self.models.CoprPermission.copr_id == self.c1.id) + .first()) + + assert new_perm.copr_builder == 1 + assert new_perm.copr_admin == 0 + + @TransactionDecorator("u1") + def test_apply_doesnt_lower_other_values_from_admin_to_request( + self, f_users, f_coprs, f_copr_permissions, f_db): + + self.db.session.add_all([self.u1, self.u2, self.cp1, self.c2]) + r = self.test_client.post("/coprs/{0}/{1}/permissions_applier_change/" + .format(self.u2.name, self.c2.name), + data={"copr_builder": 1, "copr_admin": "1"}, + follow_redirects=True) + assert "Successfuly updated" in r.data + + self.u1 = self.db.session.merge(self.u1) + self.c2 = self.db.session.merge(self.c2) + new_perm = (self.models.CoprPermission.query + .filter(self.models.CoprPermission.user_id == self.u1.id) + .filter(self.models.CoprPermission.copr_id == self.c2.id) + .first()) + + assert new_perm.copr_builder == 2 + assert new_perm.copr_admin == 1 + + +class TestCoprUpdatePermissions(CoprsTestCase): + + @TransactionDecorator("u2") + def test_cancel_permission(self, f_users, f_coprs, + f_copr_permissions, f_db): + + self.db.session.add_all([self.u2, self.c2]) + r = self.test_client.post("/coprs/{0}/{1}/update_permissions/" + .format(self.u2.name, self.c2.name), + data={"copr_builder_1": "0"}, + follow_redirects=True) + + # very volatile, but will fail fast if something changes + check_string = '' + assert check_string not in r.data + + @TransactionDecorator("u2") + def test_update_more_permissions(self, f_users, f_coprs, + f_copr_permissions, f_db): + + self.db.session.add_all([self.u2, self.c3]) + self.test_client.post("/coprs/{0}/{1}/update_permissions/" + .format(self.u2.name, self.c3.name), + data={"copr_builder_1": "2", + "copr_admin_1": "1", + "copr_admin_3": "2"}, + follow_redirects=True) + + self.u1 = self.db.session.merge(self.u1) + self.u3 = self.db.session.merge(self.u3) + self.c3 = self.db.session.merge(self.c3) + + u1_c3_perms = (self.models.CoprPermission.query + .filter(self.models.CoprPermission.copr_id == + self.c3.id) + .filter(self.models.CoprPermission.user_id == + self.u1.id) + .first()) + + assert (u1_c3_perms.copr_builder == + self.helpers.PermissionEnum("approved")) + assert (u1_c3_perms.copr_admin == + self.helpers.PermissionEnum("request")) + + u3_c3_perms = (self.models.CoprPermission.query + .filter(self.models.CoprPermission.copr_id == + self.c3.id) + .filter(self.models.CoprPermission.user_id == + self.u3.id) + .first()) + assert (u3_c3_perms.copr_builder == + self.helpers.PermissionEnum("nothing")) + assert (u3_c3_perms.copr_admin == + self.helpers.PermissionEnum("approved")) + + @TransactionDecorator("u1") + def test_copr_admin_can_update_permissions(self, f_users, f_coprs, + f_copr_permissions, f_db): + + self.db.session.add_all([self.u2, self.c3]) + r = self.test_client.post("/coprs/{0}/{1}/update_permissions/" + .format(self.u2.name, self.c3.name), + data={"copr_builder_1": "2", + "copr_admin_3": "2"}, + follow_redirects=True) + + assert "Project permissions were updated" in r.data + + @TransactionDecorator("u1") + def test_copr_admin_can_give_up_his_permissions(self, f_users, f_coprs, + f_copr_permissions, f_db): + # if admin is giving up his permission and there are more permissions for + # this copr, then if the admin is altered first, he won"t be permitted + # to alter the other permissions and the whole update would fail + self.db.session.add_all([self.u2, self.c3, self.cp2, self.cp3]) + # mock out the order of CoprPermission objects, so that we are sure + # the admin is the first one and therefore this fails if + # the view doesn"t reorder the permissions + flexmock(self.models.Copr, copr_permissions=[self.cp3, self.cp2]) + r = self.test_client.post("/coprs/{0}/{1}/update_permissions/" + .format(self.u2.name, self.c3.name), + data={"copr_admin_1": "1", + "copr_admin_3": "1"}, + follow_redirects=True) + + self.u1 = self.db.session.merge(self.u1) + self.c3 = self.db.session.merge(self.c3) + perm = (self.models.CoprPermission.query + .filter(self.models.CoprPermission.user_id == self.u1.id) + .filter(self.models.CoprPermission.copr_id == self.c3.id) + .first()) + + assert perm.copr_admin == 1 + assert "Project permissions were updated" in r.data + + +class TestCoprDelete(CoprsTestCase): + + @TransactionDecorator("u1") + def test_delete(self, f_users, f_coprs, f_db): + self.db.session.add_all([self.u1, self.c1]) + r = self.test_client.post("/coprs/{0}/{1}/delete/" + .format(self.u1.name, self.c1.name), + data={"verify": "yes"}, + follow_redirects=True) + + assert "Project was deleted successfully" in r.data + self.db.session.add(self.c1) + assert self.models.Action.query.first().id == self.c1.id + assert self.models.Copr.query.filter( + self.models.Copr.id == self.c1.id).first().deleted + + @TransactionDecorator("u1") + def test_copr_delete_does_not_delete_if_verify_filled_wrongly( + self, f_users, f_coprs, f_db): + + self.db.session.add_all([self.u1, self.c1]) + r = self.test_client.post("/coprs/{0}/{1}/delete/" + .format(self.u1.name, self.c1.name), + data={"verify": "no"}, + follow_redirects=True) + + assert "Project was deleted successfully" not in r.data + assert not self.models.Action.query.first() + assert self.models.Copr.query.filter( + self.models.Copr.id == self.c1.id).first() + + @TransactionDecorator("u2") + def test_non_owner_cant_delete(self, f_users, f_coprs, f_db): + self.db.session.add_all([self.u1, self.u2, self.c1]) + r = self.test_client.post("/coprs/{0}/{1}/delete/" + .format(self.u1.name, self.c1.name), + data={"verify": "yes"}, + follow_redirects=True) + self.c1 = self.db.session.merge(self.c1) + assert "Project was deleted successfully" not in r.data + assert not self.models.Action.query.first() + assert self.models.Copr.query.filter( + self.models.Copr.id == self.c1.id).first() + + +class TestCoprRepoGeneration(CoprsTestCase): + + @pytest.fixture + def f_custom_builds(self): + """ Custom builds are used in order not to break the default ones """ + self.b5 = self.models.Build( + copr=self.c1, user=self.u1, submitted_on=9, + ended_on=200, results="https://bar.baz") + self.b6 = self.models.Build( + copr=self.c1, user=self.u1, submitted_on=11) + self.b7 = self.models.Build( + copr=self.c1, user=self.u1, submitted_on=10, + ended_on=150, results="https://bar.baz") + self.mc1 = self.models.MockChroot( + os_release="fedora", os_version="18", arch="x86_64") + self.cc1 = self.models.CoprChroot(mock_chroot=self.mc1, copr=self.c1) + + # assign with chroots + for build in [self.b5, self.b6, self.b7]: + self.db.session.add( + self.models.BuildChroot( + build=build, + mock_chroot=self.mc1 + ) + ) + + self.db.session.add_all( + [self.b5, self.b6, self.b7, self.mc1, self.cc1]) + + @pytest.fixture + def f_not_finished_builds(self): + """ Custom builds are used in order not to break the default ones """ + self.b8 = self.models.Build( + copr=self.c1, user=self.u1, submitted_on=11) + self.mc1 = self.models.MockChroot( + os_release="fedora", os_version="18", arch="x86_64") + self.cc1 = self.models.CoprChroot(mock_chroot=self.mc1, copr=self.c1) + + # assign with chroot + self.db.session.add( + self.models.BuildChroot( + build=self.b8, + mock_chroot=self.mc1 + ) + ) + + self.db.session.add_all([self.b8, self.mc1, self.cc1]) + + def test_fail_on_missing_dash(self): + r = self.tc.get("/coprs/reponamewithoutdash/repo/") + assert r.status_code == 404 + assert "Copr with name repo does not exist" in r.data + + def test_fail_on_nonexistent_copr(self): + r = self.tc.get( + "/coprs/bogus-user/bogus-nonexistent-repo/repo/fedora-18-x86_64/") + assert r.status_code == 404 + assert "does not exist" in r.data + + def test_fail_on_no_finished_builds(self, f_users, f_coprs, + f_not_finished_builds, f_db): + + r = self.tc.get( + "/coprs/{0}/{1}/repo/fedora-18-x86_64/" + .format(self.u1.name, self.c1.name)) + + assert r.status_code == 404 + assert "Repository not initialized" in r.data + + def test_works_on_older_builds(self, f_users, f_coprs, + f_custom_builds, f_db): + r = self.tc.get( + "/coprs/{0}/{1}/repo/fedora-18-x86_64/" + .format(self.u1.name, self.c1.name)) + + assert r.status_code == 200 + assert "baseurl=https://bar.baz" in r.data diff --git a/frontend/documentation/Makefile b/frontend/documentation/Makefile new file mode 100644 index 0000000..1cfd8ec --- /dev/null +++ b/frontend/documentation/Makefile @@ -0,0 +1,34 @@ +# Copyright 2008, Steve 'Ashcrow' Milner +# +# This software may be freely redistributed under the terms of the GNU +# general public license. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + +BUILD_DIR := build + +# Python doc related +EPYDOC_BIN := `which epydoc` +EPYDOC_SWITCHES := --inheritance listed --graph all --html +PYTHON_DOC_DIR := python-doc +PYTHON_DIRS := ../coprs_frontend + +# TARGETS +# schema is not incldued here, since you need a live db +all: python + +python: + mkdir -p $(BUILD_DIR)/py-doc-build + $(SHELL ulimit -n 4096) + echo "[epydoc]" > $(BUILD_DIR)/epydoc.lst + echo -n "modules: " >> $(BUILD_DIR)/epydoc.lst + find $(PYTHON_DIRS) -type f -name '*.py' | grep -v test | xargs echo -n >> $(BUILD_DIR)/epydoc.lst + $(EPYDOC_BIN) $(EPYDOC_SWITCHES) -o $(PYTHON_DOC_DIR) --config $(BUILD_DIR)/epydoc.lst + +clean: + rm -rf build/ + +distclean: + rm -rf build/ $(PYTHON_DOC_DIR) $(JAVA_DOC_DIR) $(SCHEMA_DOC_DIR) diff --git a/frontend/documentation/how-to-generate-documentation.txt b/frontend/documentation/how-to-generate-documentation.txt new file mode 100644 index 0000000..3af5b30 --- /dev/null +++ b/frontend/documentation/how-to-generate-documentation.txt @@ -0,0 +1,10 @@ +Documentation of python: + # epydoc needs to be installed, it needs texlive, ouch :-( + # graphviz needs to be installed. + # In the Copr git repo checkout, in documentation/ directory: + git rm -rf python-doc + make python + git add python-doc + rm -rf build + # update python-doc.readme + git commit -m 'Updating python documentation.' .