diff --git a/ckanext/feedback/command/feedback.py b/ckanext/feedback/command/feedback.py new file mode 100644 index 00000000..3f1c58c9 --- /dev/null +++ b/ckanext/feedback/command/feedback.py @@ -0,0 +1,265 @@ +import sys +import psycopg2 +import click + +import ckan.plugins.toolkit as tk + + +@click.group() +def feedback(): + '''CLI tool for ckanext-feedback plugin.''' + + +def get_connection(host, port, dbname, user, password): + try: + connector = psycopg2.connect( + f'postgresql://{user}:{password}@{host}:{port}/{dbname}' + ) + except Exception as e: + tk.error_shout(e) + sys.exit(1) + else: + return connector + + +@feedback.command( + name='init', short_help='create tables in ckan db to activate modules.' +) +@click.option( + '-m', + '--modules', + multiple=True, + type=click.Choice(['utilization', 'resource', 'download']), + help='specify the module you want to use from utilization, resource, download', +) +@click.option( + '-h', + '--host', + envvar='POSTGRES_HOST', + default='db', + help='specify the host name of postgresql', +) +@click.option( + '-p', + '--port', + envvar='POSTGRES_PORT', + default=5432, + help='specify the port number of postgresql', +) +@click.option( + '-d', + '--dbname', + envvar='POSTGRES_DB', + default='ckan', + help='specify the name of postgresql', +) +@click.option( + '-u', + '--user', + envvar='POSTGRES_USER', + default='ckan', + help='specify the user name of postgresql', +) +@click.option( + '-P', + '--password', + envvar='POSTGRES_PASSWORD', + default='ckan', + help='specify the password to connect postgresql', +) +def init(modules, host, port, dbname, user, password): + with get_connection(host, port, dbname, user, password) as connection: + with connection.cursor() as cursor: + try: + if not modules: + _drop_utilization_tables(cursor) + _drop_resource_tables(cursor) + _drop_download_tables(cursor) + _create_utilization_tables(cursor) + _create_resource_tabels(cursor) + _create_download_tables(cursor) + click.secho( + 'Initialize all modules: SUCCESS', fg='green', bold=True + ) + elif 'utilization' in modules: + _drop_utilization_tables(cursor) + _create_utilization_tables(cursor) + click.secho( + 'Initialize utilization: SUCCESS', fg='green', bold=True + ) + elif 'resource' in modules: + _drop_resource_tables(cursor) + _create_resource_tabels(cursor) + click.secho('Initialize resource: SUCCESS', fg='green', bold=True) + elif 'download' in modules: + _drop_download_tables(cursor) + _create_download_tables(cursor) + click.secho('Initialize download: SUCCESS', fg='green', bold=True) + except Exception as e: + tk.error_shout(e) + sys.exit(1) + + connection.commit() + + +def _drop_utilization_tables(cursor): + cursor.execute( + ''' + DROP TABLE IF EXISTS utilization CASCADE; + DROP TABLE IF EXISTS issue_resolution_summary CASCADE; + DROP TABLE IF EXISTS issue_resolution CASCADE; + DROP TABLE IF EXISTS utilization_comment CASCADE; + DROP TABLE IF EXISTS utilization_summary CASCADE; + DROP TYPE IF EXISTS utilization_comment_category; + ''' + ) + + +def _drop_resource_tables(cursor): + cursor.execute( + ''' + DROP TABLE IF EXISTS resource_comment CASCADE; + DROP TABLE IF EXISTS resource_comment_reply CASCADE; + DROP TABLE IF EXISTS resource_comment_summary CASCADE; + DROP TYPE IF EXISTS resource_comment_category; + ''' + ) + + +def _drop_download_tables(cursor): + cursor.execute( + ''' + DROP TABLE IF EXISTS download_summary CASCADE; + ''' + ) + + +def _create_utilization_tables(cursor): + cursor.execute( + ''' + CREATE TABLE utilization ( + id TEXT NOT NULL, + resource_id TEXT NOT NULL, + title TEXT, + description TEXT, + created TIMESTAMP, + approval BOOLEAN DEFAULT false, + approved TIMESTAMP, + approval_user_id TEXT, + PRIMARY KEY (id), + FOREIGN KEY (resource_id) REFERENCES resource (id), + FOREIGN KEY (approval_user_id) REFERENCES public.user (id) + ); + + CREATE TABLE issue_resolution_summary ( + id TEXT NOT NULL, + utilization_id TEXT NOT NULL, + issue_resolution INTEGER, + created TIMESTAMP, + updated TIMESTAMP, + PRIMARY KEY (id), + FOREIGN KEY (utilization_id) REFERENCES utilization (id) + ); + + CREATE TABLE issue_resolution ( + id TEXT NOT NULL, + utilization_id TEXT NOT NULL, + description TEXT, + created TIMESTAMP, + creator_user_id TEXT, + PRIMARY KEY (id), + FOREIGN KEY (utilization_id) REFERENCES utilization (id), + FOREIGN KEY (creator_user_id) REFERENCES public.user (id) + ); + + CREATE TYPE utilization_comment_category AS ENUM ( + 'Request', 'Question', 'Advertise', 'Thank' + ); + CREATE TABLE utilization_comment ( + id TEXT NOT NULL, + utilization_id TEXT NOT NULL, + category utilization_comment_category NOT NULL, + content TEXT, + created TIMESTAMP, + approval BOOLEAN DEFAULT false, + approved TIMESTAMP, + approval_user_id TEXT, + PRIMARY KEY (id), + FOREIGN KEY (utilization_id) REFERENCES utilization (id), + FOREIGN KEY (approval_user_id) REFERENCES public.user (id) + ); + + CREATE TABLE utilization_summary ( + id TEXT NOT NULL, + resource_id TEXT NOT NULL, + utilization INTEGER, + comment INTEGER, + created TIMESTAMP, + updated TIMESTAMP, + PRIMARY KEY (id), + FOREIGN KEY (resource_id) REFERENCES resource (id) + ); + ''' + ) + + +def _create_resource_tabels(cursor): + cursor.execute( + ''' + CREATE TYPE resource_comment_category AS ENUM ( + 'Request', 'Question', 'Advertise', 'Thank' + ); + CREATE TABLE resource_comment ( + id TEXT NOT NULL, + resource_id TEXT NOT NULL, + category resource_comment_category NOT NULL, + content TEXT, + rating INTEGER, + created TIMESTAMP, + approval BOOLEAN DEFAULT false, + approved TIMESTAMP, + approval_user_id TEXT, + PRIMARY KEY (id), + FOREIGN KEY (resource_id) REFERENCES resource (id), + FOREIGN KEY (approval_user_id) REFERENCES public.user (id) + ); + + CREATE TABLE resource_comment_reply ( + id TEXT NOT NULL, + resource_comment_id TEXT NOT NULL, + content TEXT, + created TIMESTAMP, + creator_user_id TEXT, + PRIMARY KEY (id), + FOREIGN KEY (resource_comment_id) REFERENCES resource_comment (id), + FOREIGN KEY (creator_user_id) REFERENCES public.user (id) + ); + + CREATE TABLE resource_comment_summary ( + id TEXT NOT NULL, + resource_id TEXT NOT NULL, + comment INTEGER, + rating NUMERIC, + created TIMESTAMP, + updated TIMESTAMP, + PRIMARY KEY (id), + FOREIGN KEY (resource_id) REFERENCES resource (id) + ); + ''' + ) + + +def _create_download_tables(cursor): + cursor.execute( + ''' + CREATE TABLE download_summary ( + id TEXT NOT NULL, + resource_id TEXT NOT NULL, + download INTEGER, + created TIMESTAMP, + updated TIMESTAMP, + PRIMARY KEY (id), + FOREIGN KEY (resource_id) REFERENCES resource (id) + ); + ''' + ) diff --git a/ckanext/feedback/plugin.py b/ckanext/feedback/plugin.py index cbd9af3e..00332f2c 100644 --- a/ckanext/feedback/plugin.py +++ b/ckanext/feedback/plugin.py @@ -1,9 +1,11 @@ import ckan.plugins as plugins import ckan.plugins.toolkit as toolkit +from ckanext.feedback.command import feedback class FeedbackPlugin(plugins.SingletonPlugin): plugins.implements(plugins.IConfigurer) + plugins.implements(plugins.IClick) # IConfigurer @@ -12,3 +14,6 @@ def update_config(self, config_): toolkit.add_public_directory(config_, 'public') toolkit.add_resource('fanstatic', 'feedback') + + def get_commands(self): + return [feedback.feedback] \ No newline at end of file diff --git a/development/docs/README.md b/development/docs/README.md new file mode 100644 index 00000000..40103e8f --- /dev/null +++ b/development/docs/README.md @@ -0,0 +1,111 @@ +# ckan feedback init + +## 概要 + +指定した機能に関係するPostgreSQLのテーブルを初期化する。 + +## 実行 + +``` +ckan feedback init [options] +``` + +### オプション + +#### -m, --modules < utilization/ resource/ download > + +**任意項目** + +一部の機能を利用する場合に以下の3つから指定して実行する。(複数選択可) +このオプションの指定がない場合は全てのテーブルに対して初期化処理を行う。 +* utilization +* resource +* download + +##### 実行例 + +``` +# ckanext-feedback plugins に関わる全てのテーブルに対して初期化を行う +ckan --config=/etc/ckan/production.ini feedback init + +# utilization(利活用方法)機能に関わるテーブルに対して初期化を行う +ckan --config=/etc/ckan/production.ini feedback init -m utilization + +# resource(データリソース)機能に関わるテーブルに対して初期化を行う +ckan --config=/etc/ckan/production.ini feedback init -m resource + +# download(ダウンロード)機能に関わるテーブルに対して初期化を行う +ckan --config=/etc/ckan/production.ini feedback init -m download + +# resource(データリソース)機能とdownload(ダウンロード)機能に関わるテーブルに対して初期化を行う +ckan --config=/etc/ckan/production.ini feedback init -m resource -m download +``` + +※ ckanコマンドを実行する際は```--config=/etc/ckan/production.ini```と記述して、configファイルを指定する必要がある + +#### -h, --host + +**任意項目** + +PostgreSQLコンテナのホスト名を指定する。 +指定しない場合、以下の順で参照された値を使用する。 +1. 環境変数 ```POSTGRES_HOST``` +2. CKANのデフォルト値 ```db``` + +#### -p, --port + +**任意項目** + +PostgreSQLコンテナのポート番号を指定する。 +指定しない場合、以下の順で参照された値を使用する。 +1. 環境変数 ```POSTGRES_PORT``` +2. CKANのデフォルト値 ```5432``` + +#### -d, --dbname + +**任意項目** + +PostgreSQLのデータベース名を指定する。 +指定しない場合、以下の順で参照された値を使用する。 +1. 環境変数 ```POSTGRES_DB``` +2. CKANのデフォルト値 ```ckan``` + +#### -u, --user + +**任意項目** + +PostgreSQLに接続するためのユーザ名を指定する。 +指定しない場合、以下の順で参照された値を使用する。 +1. 環境変数 ```POSTGRES_USER``` +2. CKANのデフォルト値 ```ckan``` + +#### -P, --password + +**任意項目** + +PostgreSQLに接続するためのパスワードを指定する。 +指定しない場合、以下の順で参照された値を使用する。 +1. 環境変数 ```POSTGRES_PASSWORD``` +2. CKANのデフォルト値 ```ckan``` + +##### 実行例 + +``` +# ホスト名として"postgresdb"を指定する +ckan --config=/etc/ckan/production.ini feedback init -h postgresdb + +# ポート番号として"5000"を指定する +ckan --config=/etc/ckan/production.ini feedback init -p 5000 + +# データベース名として"ckandb"を指定する +ckan --config=/etc/ckan/production.ini feedback init -d ckandb + +# ユーザ名として"root"を指定する +ckan --config=/etc/ckan/production.ini feedback init -u root + +# パスワードとして"root"を指定する +ckan --config=/etc/ckan/production.ini feedback init -P root + +# ホスト名として"postgresdb", ユーザ名として"root", パスワードとして"root"を指定する +ckan --config=/etc/ckan/production.ini feedback init -h postgresdb -u root -P root +``` \ No newline at end of file diff --git a/development/misc/cli.py b/development/misc/cli.py deleted file mode 100644 index 39b4a293..00000000 --- a/development/misc/cli.py +++ /dev/null @@ -1,225 +0,0 @@ -# encoding: utf-8 - -import logging -from collections import defaultdict -from pkg_resources import iter_entry_points - -import six -import click -import sys - -import ckan.plugins as p -import ckan.cli as ckan_cli -from ckan.config.middleware import make_app -from ckan.exceptions import CkanConfigurationException -from ckan.cli import ( - config_tool, - jobs, - front_end_build, - db, search_index, server, - profile, - asset, - sysadmin, - translation, - dataset, - views, - plugin_info, - notify, - tracking, - minify, - less, - generate, - user, - create -) - -from ckan.cli import seed - -META_ATTR = u'_ckan_meta' -CMD_TYPE_PLUGIN = u'plugin' -CMD_TYPE_ENTRY = u'entry_point' - -log = logging.getLogger(__name__) - -_no_config_commands = [ - [u'config-tool'], - [u'generate', u'config'], - [u'generate', u'extension'], -] - - -class CtxObject(object): - - def __init__(self, conf=None): - # Don't import `load_config` by itself, rather call it using - # module so that it can be patched during tests - self.config = ckan_cli.load_config(conf) - self.app = make_app(self.config) - - -class ExtendableGroup(click.Group): - _section_titles = { - CMD_TYPE_PLUGIN: u'Plugins', - CMD_TYPE_ENTRY: u'Entry points', - } - - def format_commands(self, ctx, formatter): - """Print help message. - - Includes information about commands that were registered by extensions. - """ - # click won't parse config file from envvar if no other options - # provided, except for `--help`. In this case it has to be done - # manually. - if not ctx.obj: - _add_ctx_object(ctx) - _add_external_commands(ctx) - - commands = [] - ext_commands = defaultdict(lambda: defaultdict(list)) - - for subcommand in self.list_commands(ctx): - cmd = self.get_command(ctx, subcommand) - if cmd is None: - continue - if cmd.hidden: - continue - help = cmd.short_help or u'' - - meta = getattr(cmd, META_ATTR, None) - if meta: - ext_commands[meta[u'type']][meta[u'name']].append( - (subcommand, help)) - else: - commands.append((subcommand, help)) - - if commands: - with formatter.section(u'Commands'): - formatter.write_dl(commands) - - for section, group in ext_commands.items(): - with formatter.section(self._section_titles.get(section, section)): - for rows in group.values(): - formatter.write_dl(rows) - - def parse_args(self, ctx, args): - """Preprocess options and arguments. - - As long as at least one option is provided, click won't fallback to - printing help message. That means that `ckan -c config.ini` will be - executed as command, instead of just printing help message(as `ckan -c - config.ini --help`). - In order to fix it, we have to check whether there is at least one - argument. If no, let's print help message manually - - """ - result = super(ExtendableGroup, self).parse_args(ctx, args) - if not ctx.protected_args and not ctx.args: - click.echo(ctx.get_help(), color=ctx.color) - ctx.exit() - return result - - -def _init_ckan_config(ctx, param, value): - if any(sys.argv[1:len(cmd) + 1] == cmd for cmd in _no_config_commands): - return - _add_ctx_object(ctx, value) - _add_external_commands(ctx) - - -def _add_ctx_object(ctx, path=None): - """Initialize CKAN App using config file available under provided path. - - """ - try: - ctx.obj = CtxObject(path) - except CkanConfigurationException as e: - p.toolkit.error_shout(e) - ctx.abort() - - if six.PY2: - ctx.meta["flask_app"] = ctx.obj.app.apps["flask_app"]._wsgi_app - else: - ctx.meta["flask_app"] = ctx.obj.app._wsgi_app - - -def _add_external_commands(ctx): - for cmd in _get_commands_from_entry_point(): - ctx.command.add_command(cmd) - - plugins = p.PluginImplementations(p.IClick) - for cmd in _get_commands_from_plugins(plugins): - ctx.command.add_command(cmd) - - -def _command_with_ckan_meta(cmd, name, type_): - """Mark command as one retrived from CKAN extension. - - This information is used when CLI help text is generated. - """ - setattr(cmd, META_ATTR, {u'name': name, u'type': type_}) - return cmd - - -def _get_commands_from_plugins(plugins): - """Register commands that are available when plugin enabled. - - """ - for plugin in plugins: - for cmd in plugin.get_commands(): - yield _command_with_ckan_meta(cmd, plugin.name, CMD_TYPE_PLUGIN) - - -def _get_commands_from_entry_point(entry_point=u'ckan.click_command'): - """Register commands that are available even if plugin is not enabled. - - """ - registered_entries = {} - for entry in iter_entry_points(entry_point): - if entry.name in registered_entries: - p.toolkit.error_shout(( - u'Attempt to override entry_point `{name}`.\n' - u'First encounter:\n\t{first!r}\n' - u'Second encounter:\n\t{second!r}\n' - u'Either uninstall one of mentioned extensions or update' - u' corresponding `setup.py` and re-install the extension.' - ).format( - name=entry.name, - first=registered_entries[entry.name].dist, - second=entry.dist)) - raise click.Abort() - registered_entries[entry.name] = entry - - yield _command_with_ckan_meta(entry.load(), entry.name, CMD_TYPE_ENTRY) - - -@click.group(cls=ExtendableGroup) -@click.option(u'-c', u'--config', metavar=u'CONFIG', - is_eager=True, callback=_init_ckan_config, expose_value=False, - help=u'Config file to use (default: ckan.ini)') -@click.help_option(u'-h', u'--help') -def ckan(): - pass - - -ckan.add_command(jobs.jobs) -ckan.add_command(config_tool.config_tool) -ckan.add_command(front_end_build.front_end_build) -ckan.add_command(server.run) -ckan.add_command(profile.profile) -ckan.add_command(seed.seed) -ckan.add_command(db.db) -ckan.add_command(search_index.search_index) -ckan.add_command(sysadmin.sysadmin) -ckan.add_command(asset.asset) -ckan.add_command(translation.translation) -ckan.add_command(dataset.dataset) -ckan.add_command(views.views) -ckan.add_command(plugin_info.plugin_info) -ckan.add_command(notify.notify) -ckan.add_command(tracking.tracking) -ckan.add_command(minify.minify) -ckan.add_command(less.less) -ckan.add_command(generate.generate) -ckan.add_command(user.user) -ckan.add_command(create.create) diff --git a/development/misc/create.py b/development/misc/create.py deleted file mode 100644 index 0a9b34bf..00000000 --- a/development/misc/create.py +++ /dev/null @@ -1,15 +0,0 @@ -# encoding: utf-8 - -import click - -@click.group(name=u'create', short_help=u"Create database tables") -def create(): - """Create the required database tables. - """ - pass - -@create.command() -def create_tables(): - """Create the required database tables. - """ - click.secho(u'The create_tables command was executed successfully.', fg=u'green', bold=True) \ No newline at end of file diff --git a/development/setup.sh b/development/setup.sh index 99d975d9..80956584 100755 --- a/development/setup.sh +++ b/development/setup.sh @@ -4,7 +4,4 @@ git submodule update --init --recursive # Copy docker/.env.template as .env cp external/ckan/contrib/docker/.env.template external/ckan/contrib/docker/.env # Build and compose docker containers -docker compose -f external/ckan/contrib/docker/docker-compose.yml -f docker-compose.yml up --build -d -# Copy cli.py and create.py to the CKAN container ckan/cli/ folder -docker cp misc/cli.py ckan:/usr/lib/ckan/venv/src/ckan/ckan/cli/cli.py -docker cp misc/create.py ckan:/usr/lib/ckan/venv/src/ckan/ckan/cli/create.py +docker compose -f external/ckan/contrib/docker/docker-compose.yml -f docker-compose.yml up --build -d \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 3b4fac70..561c8207 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,6 +33,7 @@ python = "3.8.16" [tool.poetry.group.dev.dependencies] pyproject-flake8 = "^6.0.0.post1" +flake8-quotes = "^3.3.2" black = "^22.12.0" isort = "^5.11.4" pytest = "^7.2.1" @@ -46,6 +47,12 @@ build-backend = "poetry.core.masonry.api" [tool.flake8] max-line-length = 88 extend-ignore = "E203,W503,W504" +inline-quotes = "single" +multiline-quotes = "single" +docstring-quotes = "single" + +[tool.black] +skip-string-normalization = true [tool.isort] include_trailing_comma = true