diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 3ef741c0f..a1754b30a 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -176,9 +176,7 @@ Store the JSON file on the server and start the ASReview application from the CL ``` $ python3 -m asreview lab --flask-configfile= ``` -A number of the keys in the JSON file are standard Flask parameters. The keys that are specific for authenticating ASReview - -pare summarised below: +A number of the keys in the JSON file are standard Flask parameters. The keys that are specific for authenticating ASReview are summarised below: * AUTHENTICATION_ENABLED: if set to `true` the application will start with authentication enabled. If the SQLite database does not exist, one will be created during startup. * SECRET_KEY: the secret key is a string that is used to encrypt cookies and is mandatory if authentication is required. * SECURITY_PASSWORD_SALT: another string used to hash passwords, also mandatory if authentication is required. @@ -187,18 +185,66 @@ pare summarised below: * EMAIL_CONFIG: configuration of the SMTP email server that is used for email verification. It also allows users to retrieve a new password after forgetting it. Don't forget to enter the reply address (REPLY_ADDRESS) of your system emails. Omit this parameter if system emails for verification and password retrieval are unwanted. * OAUTH: an authenticated ASReview application may integrate with the OAuth functionality of Github, Orcid and Google. Provide the necessary OAuth login credentails (for [Github](https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/creating-an-oauth-app), [Orcid](https://info.orcid.org/documentation/api-tutorials/api-tutorial-get-and-authenticated-orcid-id/) en [Google](https://support.google.com/cloud/answer/6158849?hl=en)). Please note that the AUTHORIZATION_URL and TOKEN_URL of the Orcid entry are sandbox-urls, and thus not to be used in production. Omit this parameter if OAuth is unwanted. -### Converting an unauthenticated application in an authenticated one +### Converting an unauthenticated application into an authenticated one + +Start the application with authentication enabled for the first time. This ensures the creation of the necessary database. To avoid unwanted user input, shutdown the application. + +To convert the old unauthenticated projects into authenticated ones, the following steps should be taken: + +1. Create user accounts for people to sign in. +2. Convert project data and link the projects to the owner's user account. + +Under the CLI sub commands of the ASReview application a tool can be found that facilitates these procedures: + +``` +$ asreview auth-tool --help +``` + +#### Creating user accounts + +The first step is to create user accounts. This can be done interactively or by using a JSON string to bulk insert the accounts. To add user accounts interactively run the following command: +``` +$ asreview auth-tool add-users --db-path ~/.asreview/asreview.production.sqlite +``` + +Note that the absolute path of the sqlite database has to be provided. Also note that if your app runs in development mode, use the `asreview.development.sqlite` database instead. The tool will prompt you if you would like to add a user account. Type `Y` to continue and enter an email address, name, affiliation (not required) and a password for every person. Continue to add as many users as you would like. + +If you would like to bulk insert user accounts use the `--json` option: +``` +$ asreview auth-tool add-users -j "[{\"email\": \"name@email.org\", \"name\": \"Name of User\", \"affiliation\": \"Some Place\", \"password\": \"1234@ABcd\"}]" --db-path ~/.asreview/asreview.production.sqlite +``` +The JSON string represents a Python list with a dictionary for every user account with the following keys: `email`, `name`, `affiliation` and `password`. Note that passwords require at least one symbol. These symbols, such as the exclamation mark, may compromise the integrity of the JSON string. + +#### Preparing the projects + +After creating the user accounts, the existing projects must be stored and linked to a user account in the database. The tool provides the `list-projects` command to prepare for this step in case you would like to bulk store all projects. Ignore the following commands if you prefer to store all projects interactively. + +Without a flag, the command lists all projects: +``` +$ asreview auth-tool list-projects +``` +If you add the `--json` flag: +``` +$ asreview auth-tool list-projects --json +``` +the tool returns a convenient JSON string that can be used to bulk insert and link projects into the database. The string represents a Python list containing a dictionary for every project. Since the ID of the user account of +the owner is initially unknown, the `0` behind every `owner_id` key needs to be replaced with the appropriate owner ID. That ID number can be found if we list all user accounts with the following command: +``` +$ asreview auth-tool list-users --db-path ~/.asreview/asreview.production.sqlite +``` -At the moment there is a very basic tool to convert your unauthenticated ASReview application into an authenticated one. The following steps sketch a possible approach for the conversion: +#### Inserting and linking the projects into the database -1. In the ASReview folder (by default `~/.asreview`) you can find all projects that were created by users in the unauthenticated version. Every sub-folder contains a single project. Make sure you can link those projects to a certain user. In other words: make sure you know which project should be linked to which user. -2. Start the application, preferably with using the config JSON file and setting the ALLOW_ACCOUNT_CREATION to `true`. -3. Use the backend to create user accounts (done with a POST request to `/auth/signup`, see `/asreview/webapp/api/auth.py`). Make sure a full name is provided for every user account. Once done, one could restart the application with ALLOW_ACCOUNT_CREATION set to `False` if account creation by users is undesired. -4. Run the `auth_conversion.py` (root folder) script and follow instructions. The script iterates over all project folders in the ASReview folder and asks which user account has to be associated with it. The script will establish the connection in the SQlite database and rename the project folders accordingly. +Inserting and linking the projects into the database can be done interactively: +``` +$ asreview auth-tool link-projects --db-path ~/.asreview/asreview.production.sqlite +``` +The tool will list project by project and asks what the ID of the owner is. That ID can be found in the user list below the project information. -TODO@Jonathan @Peter: I have verified this approach. It worked for me but -obviously needs more testing. I don't think it has to grow into a bombproof solution, but should be used as a stepping stone for an admin -with a little bit of Python knowledge who wants to upgrade to an authenticated version. Anyhow: give it a spin: create a couple of projects, rename the folders in the original project_ids and remove from the projects folder. The script should restore all information. +One can also insert all project information by using the JSON string that was produced in the previous step: +``` +$ asreview auth-tool link-projects --json "[{\"folder\": \"project-id\", \"version\": \"1.1+51.g0ebdb0c.dirty\", \"project_id\": \"project-id\", \"name\": \"project 1\", \"authors\": \"Authors\", \"created\": \"2023-04-12 21:23:28.625859\", \"owner_id\": 15}]" --db-path ~/.asreview/asreview.production.sqlite +``` ## Documentation diff --git a/asreview/entry_points/__init__.py b/asreview/entry_points/__init__.py index 4a68f6e19..3f05de5d5 100644 --- a/asreview/entry_points/__init__.py +++ b/asreview/entry_points/__init__.py @@ -13,6 +13,7 @@ # limitations under the License. from asreview.entry_points.algorithms import AlgorithmsEntryPoint +from asreview.entry_points.auth_tool import AuthTool from asreview.entry_points.base import BaseEntryPoint from asreview.entry_points.lab import LABEntryPoint from asreview.entry_points.lab import WebRunModelEntryPoint diff --git a/asreview/entry_points/auth_tool.py b/asreview/entry_points/auth_tool.py new file mode 100644 index 000000000..af1b2b7d6 --- /dev/null +++ b/asreview/entry_points/auth_tool.py @@ -0,0 +1,330 @@ +import argparse +import json +from argparse import RawTextHelpFormatter +from pathlib import Path + +from sqlalchemy import create_engine +from sqlalchemy.exc import IntegrityError +from sqlalchemy.orm import sessionmaker + +from asreview.entry_points.base import BaseEntryPoint +from asreview.utils import asreview_path +from asreview.webapp.api.projects import _get_project_uuid +from asreview.webapp.authentication.models import Project +from asreview.webapp.authentication.models import User + + +def auth_parser(): + parser = argparse.ArgumentParser( + prog="auth_converter", + description="""ASReview Authentication Conversion - convert your app to handle multiple users.""", # noqa + formatter_class=RawTextHelpFormatter, + epilog="Use -h or --help on all subcommands to view the available options.", + ) + + sub_parser = parser.add_subparsers(help="The following options are available:") + + user_par = sub_parser.add_parser("add-users", help="Add users into the database.") + + user_par.add_argument( + "-d", + "--db-path", + type=str, + help="Absolute path to authentication sqlite3 database.", + required=True, + ) + + user_par.add_argument( + "-j", + "--json", + type=str, + help="JSON string that contains a list with user account data.", + ) + + list_users_par = sub_parser.add_parser( + "list-users", + help="List user accounts.", + ) + + list_users_par.add_argument( + "-d", + "--db-path", + type=str, + help="Absolute path to authentication sqlite3 database.", + required=True, + ) + + list_projects_par = sub_parser.add_parser( + "list-projects", + help="List project info from all projects in the ASReview folder.", + ) + list_projects_par.add_argument( + "-j", + "--json", + action="store_true", + help="Create JSON string to connect existing projects with users.", + ) + + link_par = sub_parser.add_parser( + "link-projects", help="Link projects to user accounts." + ) + + link_par.add_argument( + "-j", + "--json", + type=str, + help="Use a JSON string to link projects to users.", + ) + + link_par.add_argument( + "-d", + "--db-path", + type=str, + help="Absolute path to authentication sqlite3 database.", + required=True, + ) + + return parser + + +def insert_user(session, entry): + """Inserts a dictionary containing user data + into the database.""" + # create a user object + user = User( + entry["email"].lower(), + email=entry["email"].lower(), + name=entry["name"], + affiliation=entry["affiliation"], + password=entry["password"], + confirmed=True, + ) + try: + session.add(user) + session.commit() + print(f"User with email {user.email} created.") + except IntegrityError: + print(f"User with identifier {user.email} already exists") + + +def rename_project_folder(project_id, new_project_id): + """Rename folder with an authenticated project id""" + folder = asreview_path() / project_id + folder.rename(asreview_path() / new_project_id) + try: + # take care of the id inside the project.json file + with open(asreview_path() / new_project_id / "project.json", mode="r") as f: + data = json.load(f) + # change id + data["id"] = new_project_id + # overwrite original project.json file with new project id + with open(asreview_path() / new_project_id / "project.json", mode="w") as f: + json.dump(data, f) + except Exception as exc: + # revert renaming the folder + folder.rename(asreview_path() / project_id) + raise exc + + +def insert_project(session, project): + # get owner and project id + owner_id = project["owner_id"] + project_id = project["project_id"] + # create new project id + new_project_id = _get_project_uuid(project_id, owner_id) + # rename folder and project file + rename_project_folder(project_id, new_project_id) + # check if this project was already in the database under + # the old project id + db_project = ( + session.query(Project) + .filter(Project.project_id == project_id) + .one_or_none() + ) + if db_project is None: + # create new record + session.add(Project(owner_id=owner_id, project_id=new_project_id)) + else: + # update record + db_project.owner_id = owner_id + db_project.project_id = new_project_id + # commit + session.commit() + print('Project data is stored.') + + +def get_users(session): + return session.query(User).all() + + +class AuthTool(BaseEntryPoint): + def execute(self, argv): + parser = auth_parser() + args = parser.parse_args(argv) + + self.args = args + self.argv = argv + + # create a conn object for the database + if hasattr(self.args, "db_path") and self.args.db_path is not None: + Session = sessionmaker() + engine = create_engine(f"sqlite:///{self.args.db_path}") + Session.configure(bind=engine) + self.session = Session() + + if "add-users" in argv: + self.add_users() + elif "list-users" in argv: + self.list_users() + elif "list-projects" in argv: + self.list_projects() + elif "link-projects" in argv: + self.link_projects() + + return True + + def add_users(self): + if self.args.json is not None: + entries = json.loads(self.args.json) + # try to insert entries into the database + for entry in entries: + insert_user(self.session, entry) + else: + self.enter_users() + + def _ensure_valid_value_for(self, name, validation_function, hint=""): + """Prompt user for validated input.""" + while True: + value = input(f"{name}: ") + if validation_function(value): + return value + else: + print(hint) + + def enter_users(self): + while True: + new_user = input("Enter a new user [Y/n]? ") + if new_user == "Y": + email = self._ensure_valid_value_for( + "Email address (required)", + User.valid_email, + "Entered email address is not recognized as a valid email address.", # noqa + ) + name = self._ensure_valid_value_for( + "Full name (required)", + lambda x: bool(x) and len(x) > 2, + "Full name must contain more than 2 characters.", + ) + affiliation = input("Affiliation: ") + password = self._ensure_valid_value_for( + "Password (required)", + User.valid_password, + "Use 8 or more characters with a mix of letters, numbers & symbols.", # noqa + ) + + insert_user( + self.session, + { + "email": email, + "name": name, + "affiliation": affiliation, + "password": password, + } + ) + else: + break + + return True + + def _print_project(self, project): + print(f"\n* {project['folder']}") + print(f"\tversion: {project['version']}") + print(f"\tid: {project['project_id']}") + print(f"\tname: {project['name']}") + print(f"\tauthors: {project['authors']}") + print(f"\tcreated: {project['created']}") + + def _print_user(self, user): + if bool(user.affiliation): + postfix = f", {user.affiliation}" + else: + postfix = "" + print(f" {user.id} - {user.email} ({user.name}){postfix}") + + def _get_projects(self): + projects = [f for f in asreview_path().glob("*") if f.is_dir()] + result = [] + for folder in projects: + with open(Path(folder) / "project.json", "r") as out: + project = json.load(out) + + result.append( + { + "folder": folder.name, + "version": project["version"], + "project_id": project["id"], + "name": project["name"], + "authors": project["authors"], + "created": project["datetimeCreated"], + "owner_id": 0, + } + ) + return result + + def list_users(self): + users = get_users(self.session) + print() + for user in users: + self._print_user(user) + print() + + def list_projects(self): + projects = self._get_projects() + if self.args.json: + print(json.dumps(json.dumps(projects))) + else: + [self._print_project(p) for p in projects] + if len(projects) > 0: + print() + + def _generate_project_links(self): + result = [] + # get users and projects + users = get_users(self.session) + user_ids = [u.id for u in users] + projects = self._get_projects() + # print projects + for project in projects: + self._print_project(project) + print("Who's the owner of this project?") + print("--------------------------------") + for user in users: + self._print_user(user) + id = None + # and ask who the owner is + while True: + id = input("Enter the ID number of the owner: ") + try: + id = id.replace(".", "") + id = int(id) + if id not in user_ids: + print("Entered ID does not exists, try again.") + else: + insert_project( + self.session, + {"project_id": project["project_id"], "owner_id": id} + ) + break + except ValueError: + print("Entered ID is not a number, please try again.") + return result + + def link_projects(self): + # bulk JSON vs interactive + if self.args.json is not None: + projects = json.loads(self.args.json) + # enter data in the database + for project in projects: + insert_project(self.session, project) + else: + self._generate_project_links() diff --git a/asreview/webapp/authentication/models.py b/asreview/webapp/authentication/models.py index fbd984fbc..d6695344a 100644 --- a/asreview/webapp/authentication/models.py +++ b/asreview/webapp/authentication/models.py @@ -32,6 +32,9 @@ import asreview.utils as utils from asreview.webapp import DB +PASSWORD_REGEX = r"^(?=.*[A-Za-z])(?=.*\d)(?=.*[@$!%*?&#])[A-Za-z\d@$!%*?&#]{8,}$" # noqa +EMAIL_REGEX = r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,7}\b" + class User(UserMixin, DB.Model): """The User model for user accounts.""" @@ -77,12 +80,18 @@ def validate_origin(self, _key, origin): def validate_name(self, _key, name): if not bool(name): raise ValueError("Name is required") + elif len(name) < 3: + raise ValueError("Name must contain more than 2 characyers") return name @validates("email", "hashed_password") def validate_password(self, key, value): - if key == "email" and self.origin == "asreview" and bool(value) is False: - raise ValueError('Email is required when origin is "asreview"') + if key == "email" and self.origin == "asreview": + if bool(value) is False: + raise ValueError("Email is required when origin is 'asreview'") + elif not User.valid_email(value): + raise ValueError(f"Email address '{value}' is not valid") + if ( key == "hashed_password" and self.origin == "asreview" @@ -190,7 +199,13 @@ def generate_token_data(cls, secret, salt, email): @classmethod def valid_password(cls, password): return re.fullmatch( - r"^(?=.*[A-Za-z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$", password + PASSWORD_REGEX, password + ) + + @classmethod + def valid_email(cls, email): + return re.fullmatch( + EMAIL_REGEX, email ) @classmethod diff --git a/asreview/webapp/src/Components/ResetPassword.js b/asreview/webapp/src/Components/ResetPassword.js index 8b6211b00..93ab6eda7 100644 --- a/asreview/webapp/src/Components/ResetPassword.js +++ b/asreview/webapp/src/Components/ResetPassword.js @@ -71,7 +71,7 @@ const Root = styled("div")(({ theme }) => ({ const SignupSchema = Yup.object().shape({ password: Yup.string() .matches( - /^(?=.*[A-Za-z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/, + /^(?=.*[A-Za-z])(?=.*\d)(?=.*[@$!%*?&#])[A-Za-z\d@$!%*?&#]{8,}$/, 'Use 8 or more characters with a mix of letters, numbers & symbols' ) .required('Password is required'), diff --git a/asreview/webapp/src/Components/SignUpForm.js b/asreview/webapp/src/Components/SignUpForm.js index 32302ed53..3b2667c7e 100644 --- a/asreview/webapp/src/Components/SignUpForm.js +++ b/asreview/webapp/src/Components/SignUpForm.js @@ -75,7 +75,7 @@ const SignupSchema = Yup.object().shape({ .required('Affiliation is required'), password: Yup.string() .matches( - /^(?=.*[A-Za-z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/, + /^(?=.*[A-Za-z])(?=.*\d)(?=.*[@$!%*?&#])[A-Za-z\d@$!%*?&#]{8,}$/, 'Use 8 or more characters with a mix of letters, numbers & symbols' ) .required('Password is required'), diff --git a/asreview/webapp/src/HomeComponents/DashboardComponents/ProfilePage.js b/asreview/webapp/src/HomeComponents/DashboardComponents/ProfilePage.js index 94fb35606..d809e6be1 100644 --- a/asreview/webapp/src/HomeComponents/DashboardComponents/ProfilePage.js +++ b/asreview/webapp/src/HomeComponents/DashboardComponents/ProfilePage.js @@ -40,7 +40,7 @@ const SignupSchema = Yup.object().shape({ .nullable(), password: Yup.string() .matches( - /^(?=.*[A-Za-z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/, + /^(?=.*[A-Za-z])(?=.*\d)(?=.*[@$!%*?&#])[A-Za-z\d@$!%*?&#]{8,}$/, 'Use 8 or more characters with a mix of letters, numbers & symbols' ), confirmPassword: Yup.string() diff --git a/asreview/webapp/tests/test_auth_conversion.py b/asreview/webapp/tests/test_auth_conversion.py index 872cf11ce..ca2a76729 100644 --- a/asreview/webapp/tests/test_auth_conversion.py +++ b/asreview/webapp/tests/test_auth_conversion.py @@ -19,6 +19,7 @@ import pytest +from asreview.entry_points.auth_tool import insert_project from asreview.project import _create_project_id from asreview.utils import asreview_path from asreview.webapp import DB @@ -29,7 +30,6 @@ from asreview.webapp.tests.conftest import signin_user from asreview.webapp.tests.conftest import signout from asreview.webapp.tests.conftest import signup_user -from scripts.auth_conversion import main as make_links try: from .temp_env_var import TMP_ENV_VARS @@ -177,12 +177,13 @@ def test_adding_users_into_the_users_table_and_convert(self): # we want to assign project 1 to user 1 and project 2 to user 2 mapping = [ - {"user_id": user.id, "project_id": _create_project_id(PROJECTS[i]["name"])} + {"owner_id": user.id, "project_id": _create_project_id(PROJECTS[i]["name"])} for i, user in enumerate(User.query.order_by(User.id.asc()).all()) ] # execute converter with this mapping - make_links(DB.engine.raw_connection(), mapping) + for project in mapping: + insert_project(DB.session, project) # check out folders in the asreview folder folders = [f.name for f in asreview_path().glob("*") if f.is_dir()] @@ -195,7 +196,7 @@ def test_adding_users_into_the_users_table_and_convert(self): # check if we have the new folder names and if they exist # in the database with the correct user for link in mapping: - user = DB.session.get(User, link["user_id"]) + user = DB.session.get(User, link["owner_id"]) project_id = link["project_id"] # check out if the folder exists diff --git a/scripts/__init__.py b/scripts/__init__.py deleted file mode 100644 index e511769e1..000000000 --- a/scripts/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. diff --git a/scripts/auth_conversion.py b/scripts/auth_conversion.py deleted file mode 100644 index ab0a8b2e7..000000000 --- a/scripts/auth_conversion.py +++ /dev/null @@ -1,191 +0,0 @@ -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import argparse -import json -import sqlite3 -from pathlib import Path - -from asreview.utils import asreview_path -from asreview.webapp.api.projects import _get_project_uuid -from asreview.webapp.authentication.models import User - - -def get_users(conn): - """Select ids, names and emails from all users""" - qry = """SELECT id, email, name FROM users""" - cursor = conn.cursor() - cursor.execute(qry) - result = [] - for u in cursor.fetchall(): - user = User(u[1], name=u[2], email=u[1]) - user.id = u[0] - result.append(user) - return result - - -def print_user_records(users): - """Print user records from database.""" - if not bool(users): - return "No records." - print("ID\tname (email)") - print("==============================") - for user in users: - print(f"{user.id}.\t{user.name} ({user.email})") - - -def user_project_link_exists(conn, folder_id): - """Check if a project record already exists.""" - query = "SELECT COUNT(id) FROM projects " + f"WHERE project_id='{folder_id}'" - cursor = conn.cursor() - cursor.execute(query) - return cursor.fetchone()[0] == 1 - - -def link_user_to_project(conn, project_id, user_id): - """Inserts project record, links user id to project""" - query = "INSERT INTO projects(project_id, owner_id)" + "VALUES(?,?)" - cursor = conn.cursor() - cursor.execute(query, (project_id, user_id)) - conn.commit() - return True - - -def main(conn, mapping=[]): - # keep track of made links - done = [] - # get all user ids - users = get_users(conn) - # user cache to speed things up ;) - user_cache = {user.id: user for user in users} - # user ids - user_ids = list(user_cache.keys()) - - # loop over links - for link in mapping: - if link not in done: - user_id = link["user_id"] - project_id = link["project_id"] - - # see if this project is already connected to a user - if user_project_link_exists(conn, project_id): - print(f"Project {project_id} is already linked to a user") - continue - - else: - try: - # make sure user id exists - assert user_id in user_ids - - # get user record - user = user_cache[user_id] - - # create a new project_id - new_project_id = _get_project_uuid(project_id, user.id) - - # rename the folder - folder = asreview_path() / project_id - folder.rename(asreview_path() / new_project_id) - - # take care of the id inside the project.json file - with open( - asreview_path() / new_project_id / "project.json", mode="r" - ) as f: - data = json.load(f) - # change id - data["id"] = new_project_id - - # overwrite original project.json file with new project id - with open( - asreview_path() / new_project_id / "project.json", mode="w" - ) as f: - json.dump(data, f) - - # insert record - link_user_to_project( - conn, - new_project_id, - user_id, - ) - - # add to the done bucket - done.append(link) - - except AssertionError: - print(f"User id {user_id} does not exists.") - break - - -if __name__ == "__main__": - parser = argparse.ArgumentParser() - group = parser.add_mutually_exclusive_group() - group.add_argument( - "-i", - "--interactive", - action="store_true", - help="Interactively link projects to users.", - ) - group.add_argument( - "-s", - "--schema", - type=str, - default="[]", - help="JSON that links projects to user ids.", - ) - parser.add_argument( - "-d", - "--database-path", - type=str, - help="Path to sqlite database.", - required=True, - ) - args = parser.parse_args() - - # establish connect with database - conn = sqlite3.connect(asreview_path() / args.database_path) - # get all users in the user table - users = get_users(conn) - - # set up a mapping dictionary which links users with projects - mapping = [] - # iterate over all files and folders in asreview_path() - for folder in asreview_path().glob("*"): - # if folder is indeed a folder - if Path(folder).is_dir(): - # open the project.json folder - with open(folder / "project.json") as json_file: - project_data = json.load(json_file) - # get project id - project_id = project_data["id"] - - # show all users and their ids and ask who's the owner - print("\n\n==> Who is the owner of this project folder:", f"{project_id}") - print_user_records(users) - # ask who's the folder's owner - user_id = input("Provide ID number of owner > ") - user_id = user_id.replace(".", "") - - try: - # convert to integer - user_id = int(user_id) - - # add pair to the mapping - mapping.append({"user_id": user_id, "project_id": project_id}) - - except ValueError: - print("Entered input is not a string, start again.") - break - - print(mapping) - # send mapping to main to do the linking - main(conn, mapping) - - print("done.") diff --git a/setup.py b/setup.py index 26fcce79e..c9e168bfc 100644 --- a/setup.py +++ b/setup.py @@ -139,10 +139,11 @@ def get_cmdclass(): ], "asreview.entry_points": [ "lab=asreview.entry_points:LABEntryPoint", - "web_run_model = asreview.entry_points:WebRunModelEntryPoint", + "web_run_model=asreview.entry_points:WebRunModelEntryPoint", "simulate=asreview.entry_points:SimulateEntryPoint", - "algorithms = asreview.entry_points:AlgorithmsEntryPoint", - "state-inspect = asreview.entry_points:StateInspectEntryPoint", + "algorithms=asreview.entry_points:AlgorithmsEntryPoint", + "state-inspect=asreview.entry_points:StateInspectEntryPoint", + "auth-tool=asreview.entry_points:AuthTool", ], "asreview.readers": [ ".csv = asreview.io:CSVReader",