diff --git a/neuroscout/core.py b/neuroscout/core.py index 350914cac..4568f7aa7 100644 --- a/neuroscout/core.py +++ b/neuroscout/core.py @@ -53,6 +53,7 @@ ('UserTriggerResetResource', 'user/reset_password'), ('UserResetSubmitResource', 'user/submit_token'), ('UserResendConfirm', 'user/resend_confirmation'), + ('UserPredictorListResource', 'user/predictors'), ('TaskResource', 'tasks/'), ('TaskListResource', 'tasks') ]) diff --git a/neuroscout/models/predictor.py b/neuroscout/models/predictor.py index f2f44462f..2ae0df379 100644 --- a/neuroscout/models/predictor.py +++ b/neuroscout/models/predictor.py @@ -18,6 +18,9 @@ class Predictor(db.Model): predictor_events = db.relationship('PredictorEvent', backref='predictor') + predictor_collection_id = db.Column( + db.Integer, db.ForeignKey('predictor_collection.id')) + predictor_run = db.relationship('PredictorRun') active = db.Column(db.Boolean, default=True) # Actively display or not private = db.Column(db.Boolean, default=False) @@ -53,19 +56,11 @@ class PredictorRun(db.Model): primary_key=True) -# Association table between collection and predictor. -collection_predictor = db.Table( - 'collection_predictor', - db.Column('pc_id', db.Integer(), db.ForeignKey('predictor_collection.id')), - db.Column('predictor_id', db.Integer(), db.ForeignKey('predictor.id'))) - - class PredictorCollection(db.Model): """ Predictor Collection Upload """ id = db.Column(db.Integer, primary_key=True) user_id = db.Column(db.Integer, db.ForeignKey('user.id')) - predictors = db.relationship('Predictor', secondary=collection_predictor, - backref='predictor_collection') + predictors = db.relationship('Predictor', backref='predictor_collection') uploaded_at = db.Column(db.DateTime, default=datetime.datetime.utcnow) collection_name = db.Column(db.Text, nullable=False) diff --git a/neuroscout/resources/__init__.py b/neuroscout/resources/__init__.py index b211dc37b..d6b5a6daa 100644 --- a/neuroscout/resources/__init__.py +++ b/neuroscout/resources/__init__.py @@ -11,7 +11,8 @@ PredictorCollectionResource, prepare_upload) from .run import RunResource, RunListResource from .user import (UserRootResource, UserTriggerResetResource, - UserResetSubmitResource, UserResendConfirm) + UserResetSubmitResource, UserResendConfirm, + UserPredictorListResource) from .task import TaskResource, TaskListResource __all__ = [ @@ -37,6 +38,7 @@ 'UserTriggerResetResource', 'UserResetSubmitResource', 'UserResendConfirm', + 'UserPredictorListResource', 'TaskResource', 'TaskListResource', 'prepare_upload' diff --git a/neuroscout/resources/predictor.py b/neuroscout/resources/predictor.py index 2ef18dce5..db290138e 100644 --- a/neuroscout/resources/predictor.py +++ b/neuroscout/resources/predictor.py @@ -23,7 +23,7 @@ def get(self, predictor_id, **kwargs): return first_or_404(Predictor.query.filter_by(id=predictor_id)) -def get_predictors(newest=True, **kwargs): +def get_predictors(newest=True, user=None, **kwargs): """ Helper function for querying newest predictors """ if newest: predictor_ids = db.session.query( @@ -36,14 +36,21 @@ def get_predictors(newest=True, **kwargs): predictor_ids = predictor_ids.join(PredictorRun).filter( PredictorRun.run_id.in_(kwargs.pop('run_id'))) - query = Predictor.query.filter(Predictor.id.in_(predictor_ids)).filter_by( - private=False - ) + query = Predictor.query.filter(Predictor.id.in_(predictor_ids)) + for param in kwargs: query = query.filter(getattr(Predictor, param).in_(kwargs[param])) + query = query.filter_by(active=True) + + if user is not None: + query = query.filter_by(private=True).join( + PredictorCollection).filter_by(user_id=user.id) + else: + query = query.filter_by(private=False) + # Only display active predictors - return query.filter_by(active=True).all() + return query.all() class PredictorListResource(MethodResource): diff --git a/neuroscout/resources/user.py b/neuroscout/resources/user.py index 0e1ae2d03..ce516cb4f 100644 --- a/neuroscout/resources/user.py +++ b/neuroscout/resources/user.py @@ -1,5 +1,6 @@ from flask_jwt import current_identity, jwt_required from flask_security.recoverable import reset_password_token_status +import webargs as wa from flask_apispec import MethodResource, marshal_with, use_kwargs, doc from ..models.auth import User @@ -8,21 +9,21 @@ from .utils import abort, auth_required from ..utils.db import put_record from ..schemas.user import UserSchema, UserCreationSchema, UserResetSchema +from ..schemas.predictor import PredictorSchema +from .predictor import get_predictors + -# @doc(tags=['auth']) @marshal_with(UserSchema) class UserRootResource(MethodResource): - @doc(summary='Get current user information.') + @doc(tags=['user'], summary='Get current user information.') @auth_required def get(self): return current_identity - @doc(summary='Add a new user.') @use_kwargs(UserCreationSchema) def post(self, **kwargs): return register_user(**kwargs) - @doc(summary='Edit user information.') @use_kwargs(UserSchema) @auth_required def put(self, **kwargs): @@ -32,12 +33,7 @@ def put(self, **kwargs): return put_record(kwargs, current_identity) -# @doc(tags=['auth']) class UserResendConfirm(MethodResource): - @doc(summary='Resend confirmation email.') - @doc(params={"authorization": { - "in": "header", "required": True, - "description": "Format: JWT {authorization_token}"}}) @jwt_required() def post(self): if send_confirmation(current_identity): @@ -66,3 +62,23 @@ def post(self, **kwargs): user.password = kwargs['password'] db.session.commit() return {'message': 'Password reset succesfully.'} + + +class UserPredictorListResource(MethodResource): + @doc(tags=['user'], summary='Get list of user predictors.',) + @use_kwargs({ + 'run_id': wa.fields.DelimitedList( + wa.fields.Int(), description="Run id(s). Warning, slow query."), + 'name': wa.fields.DelimitedList(wa.fields.Str(), + description="Predictor name(s)"), + 'newest': wa.fields.Boolean( + missing=True, + description="Return only newest Predictor by name") + }, + locations=['query']) + @auth_required + @marshal_with(PredictorSchema(many=True)) + def get(self, **kwargs): + newest = kwargs.pop('newest') + return get_predictors(newest=newest, user=current_identity, + **kwargs) diff --git a/neuroscout/tasks/upload.py b/neuroscout/tasks/upload.py index 8826c0455..bf442baf1 100644 --- a/neuroscout/tasks/upload.py +++ b/neuroscout/tasks/upload.py @@ -66,6 +66,7 @@ def upload_collection(flask_app, filenames, runs, dataset_id, collection_id, name=col, source=f'Collection: {collection_object.collection_name}', dataset_id=dataset_id, + predictor_collection_id=collection_object.id, private=True, description=descriptions.get(col)) db.session.add(predictor) diff --git a/neuroscout/tests/api/test_predictor.py b/neuroscout/tests/api/test_predictor.py index 5b38d5d41..40fce2f2e 100644 --- a/neuroscout/tests/api/test_predictor.py +++ b/neuroscout/tests/api/test_predictor.py @@ -133,3 +133,7 @@ def test_predictor_create(session, assert resp['source'] == 'Collection: new_one' assert resp['name'] == 'trial_type' assert resp['description'] == 'new_description' + + # Test user PC route: + resp = decode_json(auth_client.get('/api/user/predictors')) + assert len(resp) == 3 diff --git a/postgres/migrations/migrations/versions/3fedbc6e3973_.py b/postgres/migrations/migrations/versions/3fedbc6e3973_.py new file mode 100644 index 000000000..e8328fd11 --- /dev/null +++ b/postgres/migrations/migrations/versions/3fedbc6e3973_.py @@ -0,0 +1,33 @@ +"""empty message + +Revision ID: 3fedbc6e3973 +Revises: 5cf42e00634a +Create Date: 2019-09-13 21:17:41.075663 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '3fedbc6e3973' +down_revision = '5cf42e00634a' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('collection_predictor') + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('collection_predictor', + sa.Column('pc_id', sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column('predictor_id', sa.INTEGER(), autoincrement=False, nullable=True), + sa.ForeignKeyConstraint(['pc_id'], ['predictor_collection.id'], name='collection_predictor_pc_id_fkey'), + sa.ForeignKeyConstraint(['predictor_id'], ['predictor.id'], name='collection_predictor_predictor_id_fkey') + ) + # ### end Alembic commands ### diff --git a/postgres/migrations/migrations/versions/5cf42e00634a_.py b/postgres/migrations/migrations/versions/5cf42e00634a_.py new file mode 100644 index 000000000..87520f042 --- /dev/null +++ b/postgres/migrations/migrations/versions/5cf42e00634a_.py @@ -0,0 +1,30 @@ +"""empty message + +Revision ID: 5cf42e00634a +Revises: 8dedbfca852e +Create Date: 2019-09-13 21:14:08.084208 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '5cf42e00634a' +down_revision = '8dedbfca852e' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('predictor', sa.Column('predictor_collection_id', sa.Integer(), nullable=True)) + op.create_foreign_key(None, 'predictor', 'predictor_collection', ['predictor_collection_id'], ['id']) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint(None, 'predictor', type_='foreignkey') + op.drop_column('predictor', 'predictor_collection_id') + # ### end Alembic commands ###