diff --git a/components/blitz/resources/omero/api/IMetadata.ice b/components/blitz/resources/omero/api/IMetadata.ice index 131edf1ea15..11bf4be434b 100644 --- a/components/blitz/resources/omero/api/IMetadata.ice +++ b/components/blitz/resources/omero/api/IMetadata.ice @@ -18,37 +18,212 @@ module omero { module api { /** - * See IMetadata.html + * Provides method to interact with acquisition metadata and + * annotations. **/ ["ami", "amd"] interface IMetadata extends ServiceInterface { + /** + * Loads the logical channels and the acquisition metadata + * related to them. + * + * @param ids The collection of logical channel's ids. + * Mustn't be null. + * @return The collection of loaded logical channels. + **/ idempotent LogicalChannelList loadChannelAcquisitionData(omero::sys::LongList ids) throws ServerError; + + /** + * Loads all the annotations of given types, that have been + * attached to the specified rootNodes for the + * specified annotatorIds. + * If no types specified, all annotations will be loaded. + * This method looks for the annotations that have been + * attached to each of the specified objects. It then maps + * each rootId onto the set of annotations + * that were found for that node. If no annotations were found + * for that node, then the entry will be null. + * Otherwise it will be a Map containing + * {@link omero.model.Annotation} objects. + * + * @param rootType + * The type of the nodes the annotations are linked to. + * Mustn't be null. + * @param rootIds + * Ids of the objects of type rootType. + * Mustn't be null. + * @param annotationType + * The types of annotation to retrieve. If + * null all annotations will be loaded. + * String of the type + * omero.model.annotations.*. + * @param annotatorIds + * Ids of the users for whom annotations should be + * retrieved. If null, all annotations + * returned. + * @param options + * @return A map whose key is rootId and value the + * Map of all annotations for that node + * or null. + **/ idempotent LongIObjectListMap loadAnnotations(string rootType, omero::sys::LongList rootIds, omero::api::StringSet annotationTypes, omero::sys::LongList annotatorIds, omero::sys::Parameters options) throws ServerError; + + /** + * Loads all the annotations of a given type. + * It is possible to filter the annotations by including or + * excluding name spaces set on the annotations. + * + * @param annotationType The type of annotations to load. + * @param include + * Include the annotations with the specified name spaces. + * @param exclude + * Exclude the annotations with the specified name spaces. + * @param options The POJO options. + * @return A collection of found annotations. + **/ idempotent AnnotationList loadSpecifiedAnnotations(string annotationType, omero::api::StringSet include, omero::api::StringSet exclude, omero::sys::Parameters options) throws ServerError; //idempotent omero::metadata::TagSetContainerList loadTagSets(long id, bool withObjects, omero::sys::Parameters options) throws ServerError; //idempotent omero::metadata::TagContainerList loadTags(long id, bool withObjects, omero::sys::Parameters options) throws ServerError; + + /** + * Loads the TagSet if the id is specified otherwise loads + * all the TagSet. + * + * @param ids The id of the tag to load or -1. + * @return Map whose key is a Tag/TagSet and the + * value either a Map or a list of related + * DataObject. + **/ idempotent LongIObjectListMap loadTagContent(omero::sys::LongList ids, omero::sys::Parameters options) throws ServerError; + + /** + * Loads all the TagSets. Returns a collection of + * AnnotationAnnotatioLink objects and, if the + * orphan parameters is true, the + * TagAnnotation object. + * Note that the difference between a TagSet and a Tag is made + * using the NS_INSIGHT_TAG_SET namespace. + * + * @param options The POJO options. + * @return See above. + **/ idempotent IObjectList loadTagSets(omero::sys::Parameters options) throws ServerError; + + /** + * Returns a map whose key is a tag id and the value the + * number of Projects, Datasets, and Images linked to that tag. + * + * @param ids The collection of ids. + * @param options The POJO options. + * @return See above. + **/ idempotent omero::sys::CountMap getTaggedObjectsCount(omero::sys::LongList ids, omero::sys::Parameters options) throws ServerError; + + /** + * Counts the number of annotation of a given type. + * + * @param annotationType The type of annotations to load. + * @param include The collection of name space, one of the + * constants defined by this class. + * @param exclude The collection of name space, one of the + * constants defined by this class. + * @param options The POJO options. + * @return See above. + **/ omero::RLong countSpecifiedAnnotations(string annotationType, omero::api::StringSet include, omero::api::StringSet exclude, omero::sys::Parameters options) throws ServerError; + + /** + * Loads the specified annotations. + * + * @param annotationIds The collection of annotation ids. + * @return See above. + */ idempotent AnnotationList loadAnnotation(omero::sys::LongList annotationIds) throws ServerError; + + /** + * Loads the instrument and its components i.e. detectors, + * objectives, etc. + * + * @param id The id of the instrument to load. + * @return See above + */ idempotent omero::model::Instrument loadInstrument(long id) throws ServerError; + + /** + * Loads the annotations of a given type used by the specified + * user but not owned by the user. + * + * @param annotationType The type of annotations to load. + * @param userID The identifier of the user. + * @return See above. + */ idempotent IObjectList loadAnnotationsUsedNotOwned(string annotationType, long userID) throws ServerError; + + /** + * Counts the number of annotation of a given type used by the + * specified user but not owned by the user. + * + * @param annotationType The type of annotations to load. + * @param userID The identifier of the user. + * @return See above. + */ omero::RLong countAnnotationsUsedNotOwned(string annotationType, long userID) throws ServerError; + + /** + * Loads the annotations of a given type linked to the + * specified objects. It is possible to filter the annotations + * by including or excluding name spaces set on the + * annotations. + * + * This method looks for the annotations that have been + * attached to each of the specified objects. It then maps + * each rootNodeId onto the set of annotations + * that were found for that node. If no annotations were found + * for that node, the map will not contain an entry for that + * node. Otherwise it will be a Set containing + * {@link omero.model.Annotation} objects. + * The rootNodeType supported are: + * Project, Dataset, Image, Pixels, Screen, Plate, + * PlateAcquisition, Well, Fileset. + * + * @param annotationType The type of annotations to load. + * @param include + * Include the annotations with the specified name spaces. + * @param exclude + * Exclude the annotations with the specified name spaces. + * @param rootNodeType + * The type of objects the annotations are linked to. + * @param rootNodeIds The identifiers of the objects. + * @param options The POJO options. + * @return A collection of found annotations. + */ idempotent LongAnnotationListMap loadSpecifiedAnnotationsLinkedTo(string annotationType, omero::api::StringSet include, omero::api::StringSet exclude, string rootNodeType, omero::sys::LongList rootNodeIds, omero::sys::Parameters options) throws ServerError; + + /** + * Finds the original file IDs for the import logs + * corresponding to the given Image or Fileset IDs. + * + * @param rootNodeType + * the root node type, may be {@link omero.model.Image} + * or {@link omero.model.Fileset} + * @param ids + * the IDs of the entities for which the import log + * original file IDs are required + * @return the original file IDs of the import logs + **/ idempotent LongIObjectListMap loadLogFiles(string rootType, omero::sys::LongList ids) throws ServerError; }; diff --git a/components/blitz/resources/omero/api/IPixels.ice b/components/blitz/resources/omero/api/IPixels.ice index c1d5297d49f..a13eaa5ef3e 100644 --- a/components/blitz/resources/omero/api/IPixels.ice +++ b/components/blitz/resources/omero/api/IPixels.ice @@ -16,19 +16,192 @@ module omero { module api { /** - * See IPixels.html + * Metadata gateway for the {@link omero.api.RenderingEngine} and + * clients. This service provides all DB access that the rendering + * engine needs as well as Pixels services to a client. It also allows + * the rendering engine to also be run external to the server (e.g. + * client-side). + * **/ ["ami", "amd"] interface IPixels extends ServiceInterface { + /** + * Retrieves the pixels metadata. The following objects are + * pre-linked: + * + * + * @param pixId Pixels id. + * @return Pixels object which matches id. + **/ idempotent omero::model::Pixels retrievePixDescription(long pixId) throws ServerError; + + /** + * Retrieves the rendering settings for a given pixels set and + * the currently logged in user. If the current user has no + * {@link omero.model.RenderingDef}, and the user is an + * administrator, then a {@link omero.model.RenderingDef} may + * be returned for the owner of the + *{@link omero.model.Pixels}. This matches the behavior of the + * Rendering service. + * + * The following objects will be pre-linked: + * + * + * @param pixId Pixels id. + * @return Rendering definition. + */ idempotent omero::model::RenderingDef retrieveRndSettings(long pixId) throws ServerError; + + /** + * Retrieves the rendering settings for a given pixels set and + * the passed user. The following objects are pre-linked: + * + * + * @param pixId Pixels id. + * @param userID The id of the user. + * @return Rendering definition. + **/ idempotent omero::model::RenderingDef retrieveRndSettingsFor(long pixId, long userId) throws ServerError; + + /** + * Retrieves all the rendering settings for a given pixels set + * and the passed user. The following objects are pre-linked: + * + * + * @param pixId Pixels id. + * @param userId The id of the user. + * @return Rendering definition. + **/ idempotent IObjectList retrieveAllRndSettings(long pixId, long userId) throws ServerError; + + /** + * Loads a specific set of rendering settings. The + * following objects are pre-linked: + * + * + * @param renderingSettingsId Rendering definition id. + * @throws ValidationException If no RenderingDef + * matches the ID renderingDefId. + * @return Rendering definition. + **/ idempotent omero::model::RenderingDef loadRndSettings(long renderingSettingsId) throws ServerError; + + /** + * Saves the specified rendering settings. + * + * @param rndSettings Rendering settings. + **/ void saveRndSettings(omero::model::RenderingDef rndSettings) throws ServerError; + + /** + * Bit depth for a given pixel type. + * + * @param type Pixels type. + * @return Bit depth in bits. + **/ idempotent int getBitDepth(omero::model::PixelsType type) throws ServerError; + + /** + * Retrieves a particular enumeration for a given enumeration + * class. + * + * @param enumClass Enumeration class. + * @param value Enumeration string value. + * @return Enumeration object. + **/ idempotent omero::model::IObject getEnumeration(string enumClass, string value) throws ServerError; + + /** + * Retrieves the exhaustive list of enumerations for a given + * enumeration class. + * + * @param enumClass Enumeration class. + * @return List of all enumeration objects for the + * enumClass. + **/ idempotent IObjectList getAllEnumerations(string enumClass) throws ServerError; + + /** + * Copies the metadata, and only the metadata linked to + * a Pixels object into a new Pixels object of equal or + * differing size across one or many of its three physical + * dimensions or temporal dimension. + * It is beyond the scope of this method to handle updates or + * changes to the raw pixel data available through + * {@link omero.api.RawPixelsStore} or to add + * and link {@link omero.model.PlaneInfo} and/or other Pixels + * set specific metadata. + * It is also assumed that the caller wishes the pixels + * dimensions and {@link omero.model.PixelsType} to remain the + * same; changing these is outside the scope of this method. + * NOTE: As {@link omero.model.Channel} objects are + * only able to apply to a single set of Pixels any + * annotations or linkage to these objects will be lost. + * + * @param pixelsId The source Pixels set id. + * @param sizeX The new size across the X-axis. + * null if the copy should maintain + * the same size. + * @param sizeY The new size across the Y-axis. + * null if the copy should maintain + * the same size. + * @param sizeZ The new size across the Z-axis. + * null if the copy should maintain + * the same size. + * @param sizeT The new number of timepoints. + * null if the copy should maintain + * the same number. + * @param channelList The channels that should be copied into + * the new Pixels set. + * @param methodology An optional string signifying the + * methodology that will be used to produce + * this new Pixels set. + * @param copyStats Whether or not to copy the + * {@link omero.model.StatsInfo} for each + * channel. + * @return Id of the new Pixels object on success or + * null on failure. + * @throws ValidationException If the X, Y, Z, T or + * channelList dimensions are out of bounds or the + * Pixels object corresponding to + * pixelsId is unlocatable. + **/ omero::RLong copyAndResizePixels(long pixelsId, omero::RInt sizeX, omero::RInt sizeY, @@ -37,6 +210,50 @@ module omero { omero::sys::IntList channelList, string methodology, bool copyStats) throws ServerError; + + /** + * Copies the metadata, and only the metadata linked to + * a Image object into a new Image object of equal or + * differing size across one or many of its three physical + * dimensions or temporal dimension. + * It is beyond the scope of this method to handle updates or + * changes to the raw pixel data available through + * {@link omero.api.RawPixelsStore} or to add + * and link {@link omero.model.PlaneInfo} and/or other Pixels + * set specific metadata. + * It is also assumed that the caller wishes the pixels + * dimensions and {@link omero.model.PixelsType} to remain the + * same; changing these is outside the scope of this method. + * NOTE: As {@link omero.model.Channel} objects are + * only able to apply to a single set of Pixels any + * annotations or linkage to these objects will be lost. + * + * @param imageId The source Image id. + * @param sizeX The new size across the X-axis. + * null if the copy should maintain + * the same size. + * @param sizeY The new size across the Y-axis. + * null if the copy should maintain + * the same size. + * @param sizeZ The new size across the Z-axis. + * null if the copy should maintain + * the same size. + * @param sizeT The new number of timepoints. + * null if the copy should maintain + * the same number. + * @param channelList The channels that should be copied into + * the new Pixels set. + * @param methodology The name of the new Image. + * @param copyStats Whether or not to copy the + * {@link omero.model.StatsInfo} for each + * channel. + * @return Id of the new Pixels object on success or + * null on failure. + * @throws ValidationException If the X, Y, Z, T or + * channelList dimensions are out of bounds or the + * Pixels object corresponding to + * pixelsId is unlocatable. + */ omero::RLong copyAndResizeImage(long imageId, omero::RInt sizeX, omero::RInt sizeY, @@ -45,10 +262,44 @@ module omero { omero::sys::IntList channelList, string methodology, bool copyStats) throws ServerError; + + /** + * Creates the metadata, and only the metadata linked + * to an Image object. It is beyond the scope of this method + * to handle updates or changes to the raw pixel data + * available through {@link omero.api.RawPixelsStore} or to + * add and link {@link omero.model.PlaneInfo} or + * {@link omero.model.StatsInfo} objects and/or other Pixels + * set specific metadata. It is also up to the caller to + * update the pixels dimensions. + * + * @param sizeX The new size across the X-axis. + * @param sizeY The new size across the Y-axis. + * @param sizeZ The new size across the Z-axis. + * @param sizeT The new number of timepoints. + * @param pixelsType The pixelsType + * @param name The name of the new Image. + * @param description The description of the new Image. + * @return Id of the new Image object on success or + * null on failure. + * @throws ValidationException If the channel list is + * null or of size == 0. + **/ omero::RLong createImage(int sizeX, int sizeY, int sizeZ, int sizeT, omero::sys::IntList channelList, omero::model::PixelsType pixelsType, string name, string description) throws ServerError; + + /** + * Sets the channel global (all 2D optical sections + * corresponding to a particular channel) minimum and maximum + * for a Pixels set. + * + * @param pixelsId The source Pixels set id. + * @param channelIndex The channel index within the Pixels set. + * @param min The channel global minimum. + * @param max The channel global maximum. + **/ void setChannelGlobalMinMax(long pixelsId, int channelIndex, double min, double max) throws ServerError; }; }; diff --git a/components/blitz/resources/omero/api/IProjection.ice b/components/blitz/resources/omero/api/IProjection.ice index 668c1b56d35..a7d2d5e4a79 100644 --- a/components/blitz/resources/omero/api/IProjection.ice +++ b/components/blitz/resources/omero/api/IProjection.ice @@ -20,15 +20,121 @@ module omero { module api { /** - * See IProjection.html + * Provides methods for performing projections of Pixels sets. **/ ["ami", "amd"] interface IProjection extends ServiceInterface { + /** + * Performs a projection through the optical sections of a + * particular wavelength at a given time point of a Pixels set. + * @param pixelsId The source Pixels set Id. + * @param pixelsType The destination Pixels type. If + * null, the source Pixels set + * pixels type will be used. + * @param algorithm MAXIMUM_INTENSITY, + * MEAN_INTENSITY or + * SUM_INTENSITY. NOTE: + * When performing a + * SUM_INTENSITY projection, + * pixel values will be pinned to the + * maximum pixel value of the destination + * Pixels type. + * @param timepoint Timepoint to perform the projection. + * @param channelIndex Index of the channel to perform the + * projection. + * @param stepping Stepping value to use while calculating the + * projection. + * For example, stepping=1 will + * use every optical section from + * start to end where + * stepping=2 will use every + * other section from start to + * end to perform the projection. + * @param start Optical section to start projecting from. + * @param end Optical section to finish projecting. + * @return A byte array of projected pixel values whose length + * is equal to the Pixels set + 8 sizeX * sizeY * bytesPerPixel in + * big-endian format. + * @throws ValidationException Where: + * + * @see #projectPixels + **/ Ice::ByteSeq projectStack(long pixelsId, omero::model::PixelsType pixelsType, omero::constants::projection::ProjectionType algorithm, int timepoint, int channelIndex, int stepping, int start, int end) throws ServerError; + + /** + * Performs a projection through selected optical sections and + * optical sections for a given set of time points of a Pixels + * set. The Image which is linked to the Pixels set will be + * copied using + * {@link omero.api.IPixels#copyAndResizeImage}. + * + * @param pixelsId The source Pixels set Id. + * @param pixelsType The destination Pixels type. If + * null, the source Pixels set + * pixels type will be used. + * @param algorithm MAXIMUM_INTENSITY, + * MEAN_INTENSITY or + * SUM_INTENSITY. NOTE: + * When performing a + * SUM_INTENSITY projection, + * pixel values will be pinned to the + * maximum pixel value of the destination + * Pixels type. + * @param tStart Timepoint to start projecting from. + * @param tEnd Timepoint to finish projecting. + * @param channels List of the channel indexes to use while + * calculating the projection. + * @param stepping Stepping value to use while calculating the + * projection. For example, + * stepping=1 will use every + * optical section from start to + * end where + * stepping=2 will use every + * other section from start to + * end to perform the projection. + * @param zStart Optical section to start projecting from. + * @param zEnd Optical section to finish projecting. + * @param name Name for the newly created image. If + * null the name of the Image linked + * to the Pixels qualified by + * pixelsId will be used with a + * "Projection" suffix. For example, + * GFP-H2B Image of HeLa Cells will have an + * Image name of + * GFP-H2B Image of HeLa Cells Projection + * used for the projection. + * @return The Id of the newly created Image which has been + * projected. + * @throws ValidationException Where: + * + * @see #projectStack + **/ long projectPixels(long pixelsId, omero::model::PixelsType pixelsType, omero::constants::projection::ProjectionType algorithm, int tStart, int tEnd, diff --git a/components/blitz/resources/omero/api/IQuery.ice b/components/blitz/resources/omero/api/IQuery.ice index 3ee5671364f..bb239fa425c 100644 --- a/components/blitz/resources/omero/api/IQuery.ice +++ b/components/blitz/resources/omero/api/IQuery.ice @@ -20,32 +20,218 @@ module omero { module api { /** - * See IQuery.html + * Provides methods for directly querying object graphs. As far as is + * possible, IQuery should be considered the lowest level DB-access + * (SELECT) interface. + * Unlike the {@link omero.api.IUpdate} interface, using other methods + * will most likely not leave the database in an inconsistent state, + * but may provide stale data in some situations. + * + * By convention, all methods that begin with get will + * never return a null or empty {@link java.util.Collection}, but + * instead will throw a {@link omero.ValidationException}. + * **/ ["ami", "amd"] interface IQuery extends ServiceInterface { + + /** + * Looks up an entity by class and id. If no such object + * exists, an exception will be thrown. + * + * @param klass the type of the entity. Not null. + * @param id the entity's id + * @return an initialized entity + * @throws ValidationException if the id doesn't exist. + **/ idempotent omero::model::IObject get(string klass, long id) throws ServerError; + + + /** + * Looks up an entity by class and id. If no such objects + * exists, return a null. + * + * @param klass klass the type of the entity. Not null. + * @param id the entity's id + * @return an initialized entity or null if id doesn't exist. + **/ idempotent omero::model::IObject find(string klass, long id) throws ServerError; + + /** + * Looks up all entities that belong to this class and match + * filter. + * + * @param klass entity type to be searched. Not null. + * @param filter filters the result set. Can be null. + * @return a collection if initialized entities or an empty + * List if none exist. + **/ idempotent IObjectList findAll(string klass, omero::sys::Filter filter) throws ServerError; + + /** + * Searches based on provided example entity. The example + * entity should uniquely specify the entity or an + * exception will be thrown. + * + * Note: findByExample does not operate on the id + * field. For that, use {@link #find}, {@link #get}, + * {@link #findByQuery}, or {@link #findAllByQuery}. + * + * @param example Non-null example object. + * @return Possibly null IObject result. + * @throws ApiUsageException if more than one result is return. + **/ idempotent omero::model::IObject findByExample(omero::model::IObject example) throws ServerError; + + /** + * Searches based on provided example entity. The returned + * entities will be limited by the {@link omero.sys.Filter} + * object. + * + * Note: findAllbyExample does not operate on the + * id field. + * For that, use {@link #find}, {@link #get}, + * {@link #findByQuery}, or {@link #findAllByQuery} + * + * + * @param example Non-null example object. + * @param filter filters the result set. Can be null. + * @return Possibly empty List of IObject results. + **/ idempotent IObjectList findAllByExample(omero::model::IObject example, omero::sys::Filter filter) throws ServerError; + + /** + * Searches a given field matching against a String. Method + * does not allow for case sensitive or insensitive + * searching since this is essentially a lookup. The existence + * of more than one result will result in an exception. + * + * @param klass type of entity to be searched + * @param field the name of the field, either as simple string + * or as public final static from the entity + * class, e.g. {@link omero.model.Project#NAME} + * @param value String used for search. + * @return found entity or possibly null. + * @throws ome.conditions.ApiUsageException + * if more than one result. + **/ idempotent omero::model::IObject findByString(string klass, string field, string value) throws ServerError; + + /** + * Searches a given field matching against a String. Method + * allows for case sensitive or insensitive searching using + * the (I)LIKE comparators. Result set will be reduced by the + * {@link omero.sys.Filter} instance. + * + * @param klass type of entity to be searched. Not null. + * @param field the name of the field, either as simple string + * or as public final static from the entity + * class, e.g. {@link omero.model.Project#NAME}. + * Not null. + * @param value String used for search. Not null. + * @param caseSensitive whether to use LIKE or ILIKE + * @param filter filters the result set. Can be null. + * @return A list (possibly empty) with the results. + **/ idempotent IObjectList findAllByString(string klass, string field, string value, bool caseSensitive, omero::sys::Filter filter) throws ServerError; + + /** + * Executes the stored query with the given name. If a query + * with the name cannot be found, an exception will be thrown. + * + * The queryName parameter can be an actual query String if the + * StringQuerySource is configured on the server and the user + * running the query has proper permissions. + * + * @param query Query to execute + * @param params + * @return Possibly null IObject result. + * @throws ValidationException + **/ idempotent omero::model::IObject findByQuery(string query, omero::sys::Parameters params) throws ServerError; + + /** + * Executes the stored query with the given name. If a query + * with the name cannot be found, an exception will be thrown. + * + * The queryName parameter can be an actual query String if the + * StringQuerySource is configured on the server and the user + * running the query has proper permissions. + * + * Queries can only return lists of + * {@link omero.model.IObject} instances. This means + * all must be of the form: + * + *
+                 * select this from SomeModelClass this ...
+                 * 
+ * + * though the alias "this" is unimportant. Do not try to + * return multiple classes in one call like: + * + *
+                 * select this, that from SomeClass this, SomeOtherClass that ...
+                 * 
+ * + * nor to project values out of an object: + * + *
+                 * select this.name from SomeClass this ...
+                 * 
+ * + * If a page is desired, add it to the query parameters. + * + * @param query Query to execute. Not null. + * @param params + * @return Possibly empty List of IObject results. + */ idempotent IObjectList findAllByQuery(string query, omero::sys::Parameters params) throws ServerError; + + /** + * Executes a full text search based on Lucene. Each term in + * the query can also be prefixed by the name of the field to + * which is should be restricted. + * + * Examples: + * + * + * For more information, see + * Query Parser Syntax + * + * The return values are first filtered by the security system. + * + * @param klass A non-null class specification of which type + * should be searched. + * @param query A non-null query string. An empty string will + * return no results. + * @param params + * Currently the parameters themselves are unused. + * But the {@link omero.sys.Parameters#theFilter} + * can be used to limit the number of results + * returned ({@link omero.sys.Filter#limit}) or the + * user for who the results will be found + * ({@link omero.sys.Filter#ownerId}). + * @return A list of loaded {@link omero.model.IObject} + * instances. Never null. + **/ idempotent IObjectList findAllByFullText(string klass, string query, omero::sys::Parameters params) throws ServerError; /** * Return a sequence of {@link omero.RType} sequences. * *

- * Each element of the outer sequence is one row in the return value. - * Each element of the inner sequence is one column specified in the HQL. + * Each element of the outer sequence is one row in the return + * value. + * Each element of the inner sequence is one column specified + * in the HQL. *

* *

* {@link omero.model.IObject} instances are returned wrapped - * in an {@link omero.rtype.RObject} instance. Primitives are + * in an {@link omero.RObject} instance. Primitives are * mapped to the expected {@link omero.RType} subclass. Types * without an {@link omero.RType} mapper if returned will * throw an exception if present in the select except where a @@ -54,17 +240,20 @@ module omero { * *

* *

- * As with SQL, if an aggregation statement is used, a group by clause must be added. + * As with SQL, if an aggregation statement is used, a group + * by clause must be added. *

* *

@@ -81,6 +270,22 @@ module omero { **/ idempotent RTypeSeqSeq projection(string query, omero::sys::Parameters params) throws ServerError; + /** + * Refreshes an entire {@link omero.model.IObject} graph, + * recursive loading all data for the managed instances in the + * graph from the database. If any non-managed entities are + * detected (e.g. without ids), an + * {@link omero.ApiUsageException} will be thrown. + * + * @param iObject Non-null managed {@link omero.model.IObject} + * graph which should have all values + * re-assigned from the database + * @return a similar {@link omero.model.IObject} graph (with + * possible additions and deletions) which is in-sync + * with the database. + * @throws ApiUsageException if any non-managed entities are + * found. + **/ idempotent omero::model::IObject refresh(omero::model::IObject iObject) throws ServerError; }; diff --git a/components/blitz/resources/omero/api/IRepositoryInfo.ice b/components/blitz/resources/omero/api/IRepositoryInfo.ice index 236b4397d89..425b286ed4c 100644 --- a/components/blitz/resources/omero/api/IRepositoryInfo.ice +++ b/components/blitz/resources/omero/api/IRepositoryInfo.ice @@ -19,14 +19,70 @@ module omero { module api { /** - * See IRepositoryInfo.html + * Provides methods for obtaining information for server repository + * disk space allocation. Could be used generically to obtain usage + * information for any mount point, however, this interface is + * prepared for the API to provide methods to obtain usage info for + * the server filesystem containing the image uploads. For the OMERO + * server base this is /OMERO. For this implementation it could be + * anything e.g. /Data1. + * + * Methods that fail or cannot execute on the server will throw an + * InternalException. This would not be normal and would indicate some + * server or disk failure. **/ ["ami", "amd"] interface IRepositoryInfo extends ServiceInterface { + /** + * Returns the total space in bytes for this file system + * including nested subdirectories. + * + * @return Total space used on this file system. + * @throws ResourceError If there is a problem retrieving disk + * space used. + **/ idempotent long getUsedSpaceInKilobytes() throws ServerError; + + /** + * Returns the free or available space on this file system + * including nested subdirectories. + * + * @return Free space on this file system in KB. + * @throws ResourceError If there is a problem retrieving disk + * space free. + **/ idempotent long getFreeSpaceInKilobytes() throws ServerError; + + /** + * Returns a double of the used space divided by the free + * space. + * This method will be called by a client to watch the + * repository filesystem so that it doesn't exceed 95% full. + * + * @return Fraction of used/free. + * @throws ResourceError If there is a problem calculating the + * usage fraction. + **/ idempotent double getUsageFraction() throws ServerError; + + /** + * Checks that image data repository has not exceeded 95% disk + * space use level. + * @throws ResourceError If the repository usage has exceeded + * 95%. + * @throws InternalException If there is a critical failure + * while sanity checking the repository. + **/ void sanityCheckRepository() throws ServerError; + + /** + * Removes all files from the server that do not have an + * OriginalFile complement in the database, all the Pixels + * that do not have a complement in the database and all the + * Thumbnail's that do not have a complement in the database. + * + * @throws ResourceError If deletion fails. + **/ void removeUnusedFiles() throws ServerError; }; diff --git a/components/blitz/src/ome/formats/importer/targets/ServerTemplateImportTarget.java b/components/blitz/src/ome/formats/importer/targets/ServerTemplateImportTarget.java index d3d37ee044a..59f28a4d8d5 100644 --- a/components/blitz/src/ome/formats/importer/targets/ServerTemplateImportTarget.java +++ b/components/blitz/src/ome/formats/importer/targets/ServerTemplateImportTarget.java @@ -101,7 +101,7 @@ public IObject load(OMEROMetadataStoreClient client, boolean spw) throws Excepti Dataset dataset; List datasets = (List) query.findAllByQuery( "select o from Dataset as o where o.name = :name" - + " order by o.id desc", + + " order by o.id " + order, new ParametersI().add("name", rstring(name))); if (datasets.size() == 0 || getDiscriminator().startsWith("@")) { dataset = new DatasetI(); diff --git a/components/blitz/src/ome/services/blitz/repo/ManagedImportRequestI.java b/components/blitz/src/ome/services/blitz/repo/ManagedImportRequestI.java index 52fc0120ac6..f3938fe78da 100644 --- a/components/blitz/src/ome/services/blitz/repo/ManagedImportRequestI.java +++ b/components/blitz/src/ome/services/blitz/repo/ManagedImportRequestI.java @@ -363,12 +363,15 @@ private void detectKnownAnnotations() throws Exception { return; } + ome.model.annotations.CommentAnnotation lastCA = null; + long maxCAId = 0; for (Annotation a : settings.userSpecifiedAnnotationList) { if (a == null) { continue; } ome.model.annotations.Annotation ann = null; + String ns = null; if (a.isLoaded()) { ns = a.getNs() == null ? null : a.getNs().getValue(); @@ -387,30 +390,36 @@ private void detectKnownAnnotations() throws Exception { if (NSAUTOCLOSE.value.equals(ns)) { autoClose = true; } else if (NSTARGETTEMPLATE.value.equals(ns)) { - ome.model.annotations.CommentAnnotation ca = - (ome.model.annotations.CommentAnnotation) ann; - if (settings.userSpecifiedTarget != null) { - // TODO: Exception - String kls = settings.userSpecifiedTarget.getClass().getSimpleName(); - long id = settings.userSpecifiedTarget.getId().getValue(); - log.error("User-specified template target '{}' AND {}:{}", - ca.getTextValue(), kls, id); - continue; + ome.model.annotations.CommentAnnotation + ca = (ome.model.annotations.CommentAnnotation) ann; + if (ca.getId().longValue() > maxCAId) { + maxCAId = ca.getId().longValue(); + lastCA = ca; } - + } + } + if (lastCA != null) { + if (settings.userSpecifiedTarget != null) { + // TODO: Exception + String kls = settings.userSpecifiedTarget.getClass().getSimpleName(); + long id = settings.userSpecifiedTarget.getId().getValue(); + log.error("User-specified template target '{}' AND {}:{}", + lastCA.getTextValue(), kls, id); + } else { // Path converted to unix slashes. - String path = ca.getDescription(); + String path = lastCA.getDescription(); File file = new File(path); for (int i = 0; i < location.omittedLevels; i++) { file = file.getParentFile(); } path = file.toString(); + log.debug("Using target path {}", path); // Here we use the client-side (but unix-separated) path, since // for simple imports, we don't have any context about the directory // from the client. ServerTemplateImportTarget target = new ServerTemplateImportTarget(path); - target.init(ca.getTextValue()); - settings.userSpecifiedTarget = target.load(store, reader.isSPWReader()); + target.init(lastCA.getTextValue()); + settings.userSpecifiedTarget = target.load(store, reader.isSPWReader()); } } } diff --git a/components/blitz/src/omero/cmd/graphs/SkipHeadI.java b/components/blitz/src/omero/cmd/graphs/SkipHeadI.java index 05a3b6661fa..b34b67d9d9e 100644 --- a/components/blitz/src/omero/cmd/graphs/SkipHeadI.java +++ b/components/blitz/src/omero/cmd/graphs/SkipHeadI.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2014-2015 University of Dundee & Open Microscopy Environment. + * Copyright (C) 2014-2016 University of Dundee & Open Microscopy Environment. * All rights reserved. * * This program is free software; you can redistribute it and/or modify @@ -29,7 +29,6 @@ import com.google.common.base.Function; import com.google.common.collect.HashMultimap; -import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.SetMultimap; @@ -37,7 +36,6 @@ import ome.services.graphs.GraphException; import ome.services.graphs.GraphPathBean; import ome.services.graphs.GraphPolicy; -import ome.system.Login; import omero.cmd.HandleI.Cancel; import omero.cmd.ERR; import omero.cmd.Helper; @@ -58,8 +56,6 @@ public class SkipHeadI extends SkipHead implements IRequest { private static final Logger LOGGER = LoggerFactory.getLogger(SkipHeadI.class); - private static final ImmutableMap ALL_GROUPS_CONTEXT = ImmutableMap.of(Login.OMERO_GROUP, "-1"); - private static final ImmutableSet REQUEST_FAILURE_FLAGS = ImmutableSet.of(State.CANCELLED, State.FAILURE); private final GraphPathBean graphPathBean; @@ -78,7 +74,6 @@ public class SkipHeadI extends SkipHead implements IRequest { * Construct a new skip-head request; called from {@link GraphRequestFactory#getRequest(Class)}. * @param graphPathBean the graph path bean to use * @param graphRequestFactory a means of instantiating the sub-request - * @throws GraphException if the request was not of an appropriate type */ public SkipHeadI(GraphPathBean graphPathBean, GraphRequestFactory graphRequestFactory) { this.graphPathBean = graphPathBean; @@ -87,7 +82,7 @@ public SkipHeadI(GraphPathBean graphPathBean, GraphRequestFactory graphRequestFa @Override public Map getCallContext() { - return new HashMap(ALL_GROUPS_CONTEXT); + return ((IRequest) request).getCallContext(); } @Override diff --git a/components/server/src/ome/security/basic/CurrentDetails.java b/components/server/src/ome/security/basic/CurrentDetails.java index 7e2a9c4ac58..28f2537b09f 100644 --- a/components/server/src/ome/security/basic/CurrentDetails.java +++ b/components/server/src/ome/security/basic/CurrentDetails.java @@ -57,7 +57,7 @@ * * This information is stored in a Details object, but unlike Details which * assumes that an empty value signifies increased security levels, empty values - * here signifiy reduced security levels. E.g., + * here signify reduced security levels. E.g., * * Details: user == null ==> object belongs to root CurrentDetails: user == null * ==> current user is "nobody" (anonymous) diff --git a/components/tests/ui/resources/web/login.txt b/components/tests/ui/resources/web/login.txt index a71ffdbcdea..e560128e6df 100644 --- a/components/tests/ui/resources/web/login.txt +++ b/components/tests/ui/resources/web/login.txt @@ -12,6 +12,7 @@ Library Selenium2Library *** Keywords *** User "${username}" logs in with password "${password}" Open Browser To Login Page + Select Server ${SERVER_ID} Input username ${username} Input password ${password} Submit credentials diff --git a/components/tests/ui/resources/web/tree.txt b/components/tests/ui/resources/web/tree.txt index b5106be419a..c0c73a47251 100644 --- a/components/tests/ui/resources/web/tree.txt +++ b/components/tests/ui/resources/web/tree.txt @@ -11,7 +11,7 @@ ${orphanedIcon} webclient/image/folder_yellow16.png ${plateIcon} webclient/image/folder_plate16.png ${runIcon} webclient/image/run16.png -${datasetDetails} xpath=//*[@id="general_tab"]/h1[contains(@data-name,'details')]/following-sibling::div +${detailsPane} xpath=//*[@id="general_tab"]/h1[contains(@data-name,'details')]/following-sibling::div *** Keywords *** Get Dialog Button Xpath @@ -136,7 +136,6 @@ Delete Container Click Dialog Button Yes # Wait for activities to show job done, then refresh tree... Wait Until Keyword Succeeds ${TIMEOUT} ${INTERVAL} Page Should Contain Element xpath=//span[@id='jobstatus'] - Wait Until Keyword Succeeds ${TIMEOUT} ${INTERVAL} Reload Page Key Down [Arguments] ${keyCode} ${cssSelector}=body @@ -280,33 +279,34 @@ Wait Until Right Panel Loads Wait Until Element Is Visible xpath=//tr[contains(@class,'data_heading_id')]/td/strong[(text() = '${containerId}')] Wait Until Right Panel Loads Everything - [Arguments] ${containerType} ${containerId} - - Wait Until Right Panel Loads ${containerType} ${containerId} - Wait Until Element Is Visible xpath=//*[@id="general_tab"]/h1[contains(text(),'${containerType} Details')] - - Run Keyword If '${containerType}' == 'Project' Wait Until Element Is Visible ${datasetDetails}//th[contains(text(),'Creation Date:')] - Run Keyword If '${containerType}' == 'Dataset' Wait Until Element Is Visible ${datasetDetails}//th[contains(text(),'Creation Date:')] - Run Keyword If '${containerType}' == 'Image' Wait Until Element Is Visible ${datasetDetails}//th[contains(text(),'Acquisition Date:')] - Run Keyword If '${containerType}' == 'Image' Wait Until Element Is Visible ${datasetDetails}//th[contains(text(),'Import Date:')] - Run Keyword If '${containerType}' == 'Image' Wait Until Element Is Visible ${datasetDetails}//th[contains(text(),'Dimensions (XY):')] - Run Keyword If '${containerType}' == 'Image' Wait Until Element Is Visible ${datasetDetails}//th[contains(text(),'Pixels Type:')] - Run Keyword If '${containerType}' == 'Image' Wait Until Element Is Visible ${datasetDetails}//th[contains(text(),'Pixels Size (XYZ) (µm):')] - Run Keyword If '${containerType}' == 'Image' Wait Until Element Is Visible ${datasetDetails}//th[contains(text(),'Z-sections/Timepoints:')] - Run Keyword If '${containerType}' == 'Image' Wait Until Element Is Visible ${datasetDetails}//th[contains(text(),'Channels:')] - Run Keyword If '${containerType}' == 'Image' Wait Until Element Is Visible ${datasetDetails}//th[contains(text(),'ROI Count:')] - Wait Until Element Is Visible xpath=//*[@id="general_tab"]/h1[contains(text(),'Tags')] - Wait Until Element Is Visible xpath=//*[@id="general_tab"]/h1[contains(text(),'Key-Value Pairs')] - Wait Until Element Is Visible xpath=//*[@id="general_tab"]/h1[contains(text(),'Attachments')] - Wait Until Element Is Visible xpath=//*[@id="general_tab"]/h1[contains(text(),'Ratings')] - Wait Until Element Is Visible xpath=//*[@id="general_tab"]/h1[contains(text(),'Comments')] - - Run Keyword If '${containerType}' == 'Image' Wait Until Element Is Visible xpath=//*[@id="general_tab"]/div/button/span[contains(text(),'Full viewer')] - Run Keyword If '${containerType}' == 'Image' Wait Until Element Is Visible xpath=//*[@id="general_tab"]/div/div/button[contains(@title,'Publishing Options')] - Run Keyword If '${containerType}' == 'Image' Wait Until Element Is Visible xpath=//*[@id="show_fs_files_btn"] - Run Keyword If '${containerType}' == 'Image' Wait Until Element Is Visible xpath=//*[@id="show_image_hierarchy"] - Run Keyword If '${containerType}' == 'Image' Wait Until Element Is Visible xpath=//*[@id="show_link_btn"] - Run Keyword If '${containerType}' == 'Image' Wait Until Element Is Visible xpath=//*[@id="general_tab"]/div/div/button[contains(@title,'Download Image as...')] + [Arguments] ${containerType} ${containerId} + + Wait Until Right Panel Loads ${containerType} ${containerId} + Wait Until Element Is Visible xpath=//*[@id="general_tab"]/h1[contains(text(),'${containerType} Details')] + Wait Until Element Is Visible xpath=//*[@id="general_tab"]/h1[contains(text(),'Tags')] + Wait Until Element Is Visible xpath=//*[@id="general_tab"]/h1[contains(text(),'Key-Value Pairs')] + Wait Until Element Is Visible xpath=//*[@id="general_tab"]/h1[contains(text(),'Attachments')] + Wait Until Element Is Visible xpath=//*[@id="general_tab"]/h1[contains(text(),'Ratings')] + Wait Until Element Is Visible xpath=//*[@id="general_tab"]/h1[contains(text(),'Comments')] + Run Keyword If '${containerType}' == 'Project' Wait Until Element Is Visible ${detailsPane}//th[contains(text(),'Creation Date:')] + Run Keyword If '${containerType}' == 'Dataset' Wait Until Element Is Visible ${detailsPane}//th[contains(text(),'Creation Date:')] + Run Keyword If '${containerType}' == 'Image' Check Right Panel Image + +Check Right Panel Image + Wait Until Element Is Visible ${detailsPane}//th[contains(text(),'Acquisition Date:')] + Wait Until Element Is Visible ${detailsPane}//th[contains(text(),'Import Date:')] + Wait Until Element Is Visible ${detailsPane}//th[contains(text(),'Dimensions (XY):')] + Wait Until Element Is Visible ${detailsPane}//th[contains(text(),'Pixels Type:')] + Wait Until Element Is Visible ${detailsPane}//th[contains(text(),'Pixels Size (XYZ) (µm):')] + Wait Until Element Is Visible ${detailsPane}//th[contains(text(),'Z-sections/Timepoints:')] + Wait Until Element Is Visible ${detailsPane}//th[contains(text(),'Channels:')] + Wait Until Element Is Visible ${detailsPane}//th[contains(text(),'ROI Count:')] + Wait Until Element Is Visible xpath=//*[@id="general_tab"]/div/a/span[contains(text(),'Full viewer')] + Wait Until Element Is Visible xpath=//*[@id="general_tab"]/div/div/button[contains(@title,'Publishing Options')] + Wait Until Element Is Visible xpath=//*[@id="show_fs_files_btn"] + Wait Until Element Is Visible xpath=//*[@id="show_image_hierarchy"] + Wait Until Element Is Visible xpath=//*[@id="show_link_btn"] + Wait Until Element Is Visible xpath=//*[@id="general_tab"]/div/div/button[contains(@title,'Download Image as...')] Wait Until Center Panel Loads [Arguments] ${containerType} diff --git a/components/tests/ui/testcases/web/create_scenario.txt b/components/tests/ui/testcases/web/create_scenario.txt index 0e93d91eb6e..339deb0fc02 100644 --- a/components/tests/ui/testcases/web/create_scenario.txt +++ b/components/tests/ui/testcases/web/create_scenario.txt @@ -62,17 +62,17 @@ Project Should Exist Wait Until Page Contains Element xpath=//li[@id='${treeRootId}']/ul/li[@id='${nodeId}'] Create Dataset Using Icon and Check If Orphaned - [Arguments] ${isOrphaned} - ${projectId}= Run Keyword If '${isOrphaned}' == '${notOrphaned}' Get Text css=tr.data_heading_id strong + [Arguments] ${isOrphaned} ${deleteDataset}=True ${projectId}=0 + # ${projectId}= Run Keyword If '${isOrphaned}' == '${notOrphaned}' Get Text css=tr.data_heading_id strong ${datasetId}= Create Dataset ${CtxDatasetName} Run Keyword If '${isOrphaned}' == '${orphaned}' Dataset Exists and Is Orphaned ${datasetId} Run Keyword If '${isOrphaned}' == '${notOrphaned}' Dataset Exists and Is Not Orphaned ${datasetId} ${projectId} - Delete Container + Run Keyword If ${deleteDataset} Delete Container [Return] ${datasetId} Create Dataset Using Right Click and Check If Orphaned - [Arguments] ${RootId} ${DatasetName} ${isOrphaned} - ${projectId}= Run Keyword If '${isOrphaned}' == '${notOrphaned}' Get Text css=tr.data_heading_id strong + [Arguments] ${RootId} ${DatasetName} ${isOrphaned} ${projectId}=0 + # ${projectId}= Run Keyword If '${isOrphaned}' == '${notOrphaned}' Get Text css=tr.data_heading_id strong Right Click Create P/D/S ${RootId} Dataset ${DatasetName} ${datasetId}= Check Right And Center Panels For Active Container Dataset ${DatasetName} Run Keyword If '${isOrphaned}' == '${orphaned}' Dataset Exists and Is Orphaned ${datasetId} @@ -131,17 +131,17 @@ Test Container Creation Enabled Create Button Should Be Enabled project Create Button Should Be Enabled dataset Create Button Should Be Enabled screen - Node Popup Menu Item Should Be Disabled Project + Node Popup Menu Item Should Be Enabled Project Node Popup Menu Item Should Be Enabled Dataset - Node Popup Menu Item Should Be Disabled Screen + Node Popup Menu Item Should Be Enabled Screen Node Popup Menu Item Should Be Enabled Delete Select First Dataset With Children Create Button Should Be Enabled project Create Button Should Be Enabled dataset Create Button Should Be Enabled screen - Node Popup Menu Item Should Be Disabled Project - Node Popup Menu Item Should Be Disabled Dataset - Node Popup Menu Item Should Be Disabled Screen + Node Popup Menu Item Should Be Enabled Project + Node Popup Menu Item Should Be Enabled Dataset + Node Popup Menu Item Should Be Enabled Screen Node Popup Menu Item Should Be Enabled Delete ${imageId}= Select First Image Create Button Should Be Enabled project @@ -151,7 +151,16 @@ Test Container Creation Enabled Node Popup Menu Item Should Be Enabled Dataset Node Popup Menu Item Should Be Enabled Screen Node Popup Menu Item Should Be Enabled Delete + Select Orphaned Images Section + Create Button Should Be Enabled project + Create Button Should Be Enabled dataset + Create Button Should Be Enabled screen + Node Popup Menu Item Should Be Enabled Project + Node Popup Menu Item Should Be Enabled Dataset + Node Popup Menu Item Should Be Enabled Screen + Node Popup Menu Item Should Be Disabled Delete + ${imageId}= Select First Image Thumbnail Should Be Selected ${imageId} ${imageNodeId}= Select Image By Id ${imageId} # Using hot-key fails on jsTree 3 (worked OK previously)... @@ -215,11 +224,16 @@ Test Create Dataset Using Icon Select Experimenter Create Dataset Using Icon and Check If Orphaned ${orphaned} - Select First Project - Create Dataset Using Icon and Check If Orphaned ${notOrphaned} + # We don't delete new Dataset 'False'. Get datasetId and delete below + ${pId}= Select First Project + ${dId}= Create Dataset Using Icon and Check If Orphaned ${notOrphaned} False ${pId} - Select First Dataset - Create Dataset Using Icon and Check If Orphaned ${orphaned} + # Dataset just-created is still selected. Create sibling Dataset (with Delete) + Create Dataset Using Icon and Check If Orphaned ${notOrphaned} True ${pId} + + # Now select and Delete the new Dataset we created under Project + Select Dataset By Id ${dId} + Delete Container Select First Screen Create Dataset Using Icon and Check If Orphaned ${orphaned} @@ -240,7 +254,15 @@ Test Create Dataset Using Right Click ${pId}= Select First Project ${treeId}= Wait For Project Node ${pId} - Create Dataset Using Right Click and Check If Orphaned ${treeId} ${CtxDatasetName} ${notOrphaned} + ${dId}= Create Dataset Using Right Click and Check If Orphaned ${treeId} ${CtxDatasetName} ${notOrphaned} ${pId} + # Don't Delete new Dataset here - Test creating next Dataset as sibling... + + ${treeId}= Wait For Dataset Node ${dId} + Create Dataset Using Right Click and Check If Orphaned ${treeId} ${CtxDatasetName} ${notOrphaned} ${pId} + Delete Container + + # Now select and Delete the new Dataset we created under Project + Select Dataset By Id ${dId} Delete Container ${imageId}= Select And Expand Image diff --git a/components/tests/ui/testcases/web/post_test.txt b/components/tests/ui/testcases/web/post_test.txt index 34bbd1a73e9..4f2ecbe0f2e 100644 --- a/components/tests/ui/testcases/web/post_test.txt +++ b/components/tests/ui/testcases/web/post_test.txt @@ -36,10 +36,11 @@ Test Cut Paste Orphaned Image ${nodeId} Select Orphaned Images Section ${imageId} Select First Orphaned Image Click Element id=cutButton - Wait Until Keyword Succeeds ${TIMEOUT} ${INTERVAL} Page Should Not Contain Element xpath=//li[@id='${nodeId}']//li[@data-id='${imageId}'] + Page Should Contain Element xpath=//li[@id='${nodeId}']//li[@data-id='${imageId}'] ${dId} Select First Dataset Click Element id=pasteButton Dataset Should Contain Image ${imageId} ${dId} + Page Should Not Contain Element xpath=//li[@id='${nodeId}']//li[@data-id='${imageId}'] Test Cut Paste Dataset [Documentation] Create 2 Projects and a Dataset. Cut and Paste the Dataset. @@ -173,25 +174,6 @@ Test Copy Paste Image Select Image By Id ${imageId} Delete Container -Test Copy Paste Orphaned Image - [Documentation] Test copy pasting an orphaned Image into a Dataset. Check if the link exists on Both locations. - - Wait Until Keyword Succeeds ${TIMEOUT} ${INTERVAL} Reload Page - Select Experimenter - ${nodeId} Select Orphaned Images Section - ${imageId} Select First Orphaned Image - Click Element id=copyButton - ${dnodeId} Wait For Dataset Node Text test copy-paste TO here - Click Node ${dnodeId} - Click Element id=pasteButton - - Click Node ${dnodeId} - Wait Until Page Contains Element xpath=//li[@id='${nodeId}']//li[@data-id='${imageId}'] - Wait Until Page Contains Element xpath=//li[@id='${dnodeId}']//li[@data-id='${imageId}'] - - Select Image By Id ${imageId} - Delete Container - Test Copy Paste Plate [Documentation] Test copy pasting a plate into another screen. Check if the link exists on both the screens. diff --git a/components/tests/ui/testcases/web/rdef_test.txt b/components/tests/ui/testcases/web/rdef_test.txt index 36ef6cf3ef1..5bfb55c05cc 100644 --- a/components/tests/ui/testcases/web/rdef_test.txt +++ b/components/tests/ui/testcases/web/rdef_test.txt @@ -239,9 +239,9 @@ Test Owners Rdef # Test 'Paste and Save' with right-click on different Image in tree # (check thumb refresh by change of src) - ${thumbSrc}= Get Element Attribute xpath=//li[@id="image_icon-${imageId_2}"]/div[@class="image"]/img@src + ${thumbSrc}= Get Element Attribute xpath=//li[@id="image_icon-${imageId_2}"]/div[@class="image"]/a/img@src Right Click Image Rendering Settings ${imageId_2} Paste and Save - Wait Until Page Contains Element xpath=//li[@id="image_icon-${imageId_2}"]/div[@class="image"]/img[@src!='${thumbSrc}'] + Wait Until Page Contains Element xpath=//li[@id="image_icon-${imageId_2}"]/div[@class="image"]/a/img[@src!='${thumbSrc}'] # Check applied by refresh right panel Select Image By Id ${imageId_2} ${status} ${oldId} Wait For Preview Load ${status} ${oldId} @@ -249,9 +249,9 @@ Test Owners Rdef Textfield Value Should Be wblitz-ch0-cw-end 200 # Test Set Owner's in same way on first Image - ${thumbSrc}= Get Element Attribute xpath=//li[@id="image_icon-${imageId}"]/div[@class="image"]/img@src + ${thumbSrc}= Get Element Attribute xpath=//li[@id="image_icon-${imageId}"]/div[@class="image"]/a/img@src Right Click Image Rendering Settings ${imageId} Set Owner's and Save - Wait Until Page Contains Element xpath=//li[@id="image_icon-${imageId}"]/div[@class="image"]/img[@src!='${thumbSrc}'] + Wait Until Page Contains Element xpath=//li[@id="image_icon-${imageId}"]/div[@class="image"]/a/img[@src!='${thumbSrc}'] # Check applied by refresh right panel Click Element id=image_icon-${imageId} ${status} ${oldId} Wait For Preview Load ${status} ${oldId} @@ -259,9 +259,9 @@ Test Owners Rdef Textfield Value Should Be wblitz-ch0-cw-end 100 # Test "Set Imported" on first Image - ${thumbSrc}= Get Element Attribute xpath=//li[@id="image_icon-${imageId}"]/div[@class="image"]/img@src + ${thumbSrc}= Get Element Attribute xpath=//li[@id="image_icon-${imageId}"]/div[@class="image"]/a/img@src Right Click Image Rendering Settings ${imageId} Set Imported and Save - Wait Until Page Contains Element xpath=//li[@id="image_icon-${imageId}"]/div[@class="image"]/img[@src!='${thumbSrc}'] + Wait Until Page Contains Element xpath=//li[@id="image_icon-${imageId}"]/div[@class="image"]/a/img[@src!='${thumbSrc}'] # Check applied by refresh right panel Click Element id=image_icon-${imageId} ${status} ${oldId} Wait For Preview Load ${status} ${oldId} diff --git a/components/tests/ui/testcases/web/view_image.txt b/components/tests/ui/testcases/web/view_image.txt index 4b22532e12e..7a5ee8e2c6f 100644 --- a/components/tests/ui/testcases/web/view_image.txt +++ b/components/tests/ui/testcases/web/view_image.txt @@ -39,7 +39,7 @@ Test Open Viewer ${nodeId}= Wait For Image Node ${imageId} ${imageName}= Wait For General Panel And Return Name Image # Open Image Viewer 3 different ways and check - Click Element xpath=//button[@title='Open full image viewer in new window'] + Click Element xpath=//a[@title='Open full image viewer in new window'] Check Image Viewer ${imageName} Double Click Element xpath=//li[@id='image_icon-${imageId}']//img Check Image Viewer ${imageName} diff --git a/components/tools/OmeroJava/test/integration/DuplicationTest.java b/components/tools/OmeroJava/test/integration/DuplicationTest.java index f87f87764d1..5d2f35d9397 100644 --- a/components/tools/OmeroJava/test/integration/DuplicationTest.java +++ b/components/tools/OmeroJava/test/integration/DuplicationTest.java @@ -39,6 +39,7 @@ import omero.cmd.DuplicateResponse; import omero.cmd.ERR; import omero.cmd.Response; +import omero.cmd.SkipHead; import omero.gateway.util.Requests; import omero.gateway.util.Requests.DuplicateBuilder; import omero.model.Annotation; @@ -1228,6 +1229,54 @@ public void testDuplicateImageWithOpposingTypeOptions() throws Exception { } } + /** + * Test duplication of a dataset's image. {@link SkipHead} is used to identify the image via its dataset. + * @throws Exception unexpected + */ + @Test(groups = "ticket:13197") + public void testDuplicateImageViaSkipHead() throws Exception { + newUserAndGroup("rwr---"); + + /* create a dataset with an image */ + + final Dataset originalDataset = mmFactory.simpleDataset(); + final Image originalImage = mmFactory.simpleImage(); + DatasetImageLink originalLink = new DatasetImageLinkI(); + originalLink.setParent(originalDataset); + originalLink.setChild(originalImage); + originalLink = (DatasetImageLink) iUpdate.saveAndReturnObject(originalLink); + + /* note the objects (and their IDs) that were thus created and saved */ + + final long originalDatasetId = originalLink.getParent().getId().getValue(); + final long originalImageId = originalLink.getChild().getId().getValue(); + final long originalLinkId = originalLink.getId().getValue(); + testImages.add(originalImageId); + + /* duplicate the image via SkipHead */ + + final SkipHead dup = + Requests.skipHead().target("Dataset").id(originalDatasetId).startFrom("Image").request(Duplicate.class).build(); + final DuplicateResponse response = (DuplicateResponse) doChange(dup); + + /* check that the response includes duplication of only the image */ + + final List reportedDatasetIds = response.duplicates.get("ome.model.containers.Dataset"); + final List reportedImageIds = response.duplicates.get("ome.model.core.Image"); + final List reportedLinkIds = response.duplicates.get("ome.model.annotations.ImageAnnotationLink"); + + Assert.assertNull(reportedDatasetIds); + Assert.assertEquals(reportedImageIds.size(), 1); + Assert.assertNull(reportedLinkIds); + + /* check that the reported image has a new ID */ + + final long reportedImageId = reportedImageIds.get(0); + testImages.add(reportedImageId); + + Assert.assertNotEquals(originalImageId, reportedImageId); + } + /** * Test duplication of links depending on ownership and group permissions. * @param groupPerms the permissions on the group in which to test diff --git a/components/tools/OmeroM/src/annotations/getFileAnnotationContent.m b/components/tools/OmeroM/src/annotations/getFileAnnotationContent.m index b91fc09ee65..ae4a1cc4924 100644 --- a/components/tools/OmeroM/src/annotations/getFileAnnotationContent.m +++ b/components/tools/OmeroM/src/annotations/getFileAnnotationContent.m @@ -41,8 +41,6 @@ function getFileAnnotationContent(session, fileAnnotation, path) ip.addParamValue('group', [], @(x) isscalar(x) && isnumeric(x)); ip.parse(fileAnnotation, path); -context = java.util.HashMap; -context.put('omero.group', '-1'); if ~isa(fileAnnotation, 'omero.model.FileAnnotationI'), % Load the file annotation from the server @@ -52,17 +50,5 @@ function getFileAnnotationContent(session, fileAnnotation, path) 'Could not load the file annotation: %u', faID); end -% Initialize raw file store -store = session.createRawFileStore(); +getOriginalFileContent(session, fileAnnotation.getFile(), path); -% Set file annotation id -file = fileAnnotation.getFile(); -store.setFileId(file.getId().getValue(), context); - -% Read data and cast into int8 -fid = fopen(path, 'w'); -fwrite(fid, store.read(0, file.getSize().getValue()), 'int8'); -fclose(fid); - -% Close the file store -store.close() \ No newline at end of file diff --git a/components/tools/OmeroM/src/annotations/getOriginalFileContent.m b/components/tools/OmeroM/src/annotations/getOriginalFileContent.m new file mode 100644 index 00000000000..fe29d516193 --- /dev/null +++ b/components/tools/OmeroM/src/annotations/getOriginalFileContent.m @@ -0,0 +1,49 @@ +function getOriginalFileContent(session, originalFile, path) +% GETORIGINALFILECONTENT Reads the file content of an OriginalFile obtained +% from a file annotation +% +% getOriginalFileContent(session, originalFile, path) reads the file +% content of the input originalfile and saves it to the file +% specified by the input path. +% +% +% +% See also: GETFILEANNOTATIONCONTENT + +% Copyright (C) 2016 University of Dundee & Open Microscopy Environment. +% All rights reserved. +% +% 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. + +% Input check +assert(isa(originalFile, 'omero.model.OriginalFileI'),'Not an original file:'); + +context = java.util.HashMap; +context.put('omero.group', '-1'); + +% Initialize raw file store +store = session.createRawFileStore(); + +% Set file annotation id +store.setFileId(originalFile.getId().getValue(), context); + +% Read data and cast into int8 +fid = fopen(path, 'w'); +byteArr = store.read(0,originalFile.getSize().getValue()); +fwrite(fid,byteArr,'*uint8'); +fclose(fid); + +% Close the file store +store.close(); \ No newline at end of file diff --git a/components/tools/OmeroPy/test/integration/clitest/test_import.py b/components/tools/OmeroPy/test/integration/clitest/test_import.py index 70f7df1341b..beb1e93f596 100644 --- a/components/tools/OmeroPy/test/integration/clitest/test_import.py +++ b/components/tools/OmeroPy/test/integration/clitest/test_import.py @@ -156,6 +156,20 @@ def get_object(self, err, obj_type, query=None): return query.get(obj_type, int(match.group('id')), {"omero.group": "-1"}) + def get_objects(self, err, obj_type, query=None): + if not query: + query = self.query + """Retrieve the created objects by parsing the stderr output""" + pattern = re.compile('^%s:(?P\d+)$' % obj_type) + objs = [] + for line in reversed(err.split('\n')): + match = re.match(pattern, line) + if match: + objs.append( + query.get(obj_type, int(match.group('id')), + {"omero.group": "-1"})) + return objs + def get_linked_annotations(self, oid): """Retrieve the comment annotation linked to the image""" @@ -189,6 +203,14 @@ def get_screen(self, pid): query += " where l.child.id=:id and l.parent=d.id) " return self.query.findByQuery(query, params) + def get_container(self, pid, spw=False): + """Retrieve the single container linked to an image or plate""" + + if spw: + return self.get_screen(pid) + else: + return self.get_dataset(pid) + def get_screens(self, pid): """Retrieve the screens linked to the plate""" @@ -621,6 +643,116 @@ def testUniqueMultipleNameModelTargets(self, spw, tmpdir, capfd): with pytest.raises(NonZeroReturnCode): self.do_import(capfd) + @pytest.mark.parametrize("spw", (True, False)) + def testNestedNameTemplateTargetArgument( + self, spw, tmpdir, capfd): + + outer = "NestedNameTemplateTargetArgument-Test-" + self.uuid() + inner1 = "NestedNameTemplateTargetArgument-Test-" + self.uuid() + inner2 = "NestedNameTemplateTargetArgument-Test-" + self.uuid() + subdir = tmpdir.mkdir(outer) + if spw: + importType = "Plate" + fake = ("SPW&screens=0&plates=1&plateRows=1&plateCols=1&" + "fields=1&plateAcqs=1.fake") + else: + importType = "Image" + fake = "test.fake" + subdir.mkdir(inner1).join(fake).write('') + subdir.mkdir(inner2).join(fake).write('') + + self.args += ("-T", "regex:name:^.*%s/(?.*)" % outer) + self.args += [str(tmpdir)] + + # Now, run the import and check that two distinct + # containers are created and used. + o, e = self.do_import(capfd) + + objs = self.get_objects(e, importType) + assert len(objs) == 2 + container1 = self.get_container(objs[0].id.val, spw=spw) + container2 = self.get_container(objs[1].id.val, spw=spw) + assert container1.id.val != container2.id.val + if container1.name.val == inner1: + assert container2.name.val == inner2 + else: + assert container1.name.val == inner2 + assert container2.name.val == inner1 + + @pytest.mark.parametrize("spw", (True, False)) + @pytest.mark.parametrize("qualifier", ("", "+", "-", "@")) + def testMultipleNameTemplateTargetArgument( + self, spw, qualifier, tmpdir, capfd): + + outer = "MultipleNameTemplateTargetArgument-Test-" + self.uuid() + inner = "MultipleNameTemplateTargetArgument-Test-" + self.uuid() + oids = [] + for i in range(2): + if spw: + kls = "Screen" + else: + kls = "Dataset" + oid = self.create_object(kls, name=inner) + oids.append(oid) + + subdir = tmpdir.mkdir(outer) + if spw: + fake = ("SPW&screens=0&plates=1&plateRows=1&plateCols=1&" + "fields=1&plateAcqs=1.fake") + else: + fake = "test.fake" + subdir.mkdir(inner).join(fake).write('') + + self.args += ("-T", + ("regex:%sname:^.*%s/(?.*)" + % (qualifier, outer))) + self.args += [str(tmpdir)] + + # Now, run the import and check that the correct + # container is used or created and used. + found = self.parse_container(spw, capfd) + if qualifier == "-": + assert found == min(oids) + elif qualifier == "@": + assert found not in oids + else: + assert found == max(oids) + + @pytest.mark.parametrize("spw", (True, False)) + def testUniqueMultipleNameTemplateTargetArgument( + self, spw, tmpdir, capfd): + + outer = "UniqueMultipleNameTemplateTargetArgument-Test-" + self.uuid() + inner = "UniqueMultipleNameTemplateTargetArgument-Test-" + self.uuid() + oids = [] + for i in range(2): + if spw: + kls = "Screen" + else: + kls = "Dataset" + oid = self.create_object(kls, name=inner) + oids.append(oid) + + subdir = tmpdir.mkdir(outer) + if spw: + importType = "Plate" + fake = ("SPW&screens=0&plates=1&plateRows=1&plateCols=1&" + "fields=1&plateAcqs=1.fake") + else: + importType = "Image" + fake = "test.fake" + subdir.mkdir(inner).join(fake).write('') + + self.args += ("-T", "regex:%%name:^.*%s/(?.*)" % outer) + self.args += [str(tmpdir)] + + # Now, run the import and check that the imported object + # is not in a container. + o, e = self.do_import(capfd) + obj = self.get_object(e, importType) + container = self.get_container(obj.id.val, spw=spw) + assert container is None + @pytest.mark.parametrize("kls", ("Project", "Plate", "Image")) def testBadTargetArgument(self, kls, tmpdir, capfd): diff --git a/components/tools/OmeroWeb/omeroweb/webclient/controller/container.py b/components/tools/OmeroWeb/omeroweb/webclient/controller/container.py index 6fea1e1e1dc..1c43cc214f1 100644 --- a/components/tools/OmeroWeb/omeroweb/webclient/controller/container.py +++ b/components/tools/OmeroWeb/omeroweb/webclient/controller/container.py @@ -117,6 +117,8 @@ def __init__(self, conn, project=None, dataset=None, image=None, if tagset is not None: self.obj_type = "tagset" self.tag = self.conn.getObject("Annotation", tagset) + # We need to check if tagset via hasattr(manager, o_type) + self.tagset = self.tag self.assertNotNone(self.tag, tagset, "Tag") self.assertNotNone(self.tag._obj, tagset, "Tag") if comment is not None: @@ -809,6 +811,19 @@ def createProject(self, name, description=None): def createScreen(self, name, description=None): return self.conn.createScreen(name, description) + def createTag(self, name, description=None): + tId = self.conn.createTag(name, description) + if (self.tag and + self.tag.getNs() == omero.constants.metadata.NSINSIGHTTAGSET): + link = omero.model.AnnotationAnnotationLinkI() + link.setParent(omero.model.TagAnnotationI(self.tag.getId(), False)) + link.setChild(omero.model.TagAnnotationI(tId, False)) + self.conn.saveObject(link) + return tId + + def createTagset(self, name, description=None): + return self.conn.createTagset(name, description) + def checkMimetype(self, file_type): if file_type is None or len(file_type) == 0: file_type = "application/octet-stream" diff --git a/components/tools/OmeroWeb/omeroweb/webclient/static/webclient/css/dusty.css b/components/tools/OmeroWeb/omeroweb/webclient/static/webclient/css/dusty.css index ec2fb7ee0e1..78871dc8302 100755 --- a/components/tools/OmeroWeb/omeroweb/webclient/static/webclient/css/dusty.css +++ b/components/tools/OmeroWeb/omeroweb/webclient/static/webclient/css/dusty.css @@ -440,7 +440,7 @@ button::-moz-focus-inner { */ - .tag { +.tag_annotation_wrapper .tag { display:inline-block; float:left; margin:1px 1px; diff --git a/components/tools/OmeroWeb/omeroweb/webclient/static/webclient/image/left_sidebar_icon_tag_locked.png b/components/tools/OmeroWeb/omeroweb/webclient/static/webclient/image/left_sidebar_icon_tag_locked.png deleted file mode 100644 index 351ebbec6a7..00000000000 Binary files a/components/tools/OmeroWeb/omeroweb/webclient/static/webclient/image/left_sidebar_icon_tag_locked.png and /dev/null differ diff --git a/components/tools/OmeroWeb/omeroweb/webclient/static/webclient/image/left_sidebar_icon_tags.png b/components/tools/OmeroWeb/omeroweb/webclient/static/webclient/image/left_sidebar_icon_tags.png index 82a801efd1f..caec5ec90df 100644 Binary files a/components/tools/OmeroWeb/omeroweb/webclient/static/webclient/image/left_sidebar_icon_tags.png and b/components/tools/OmeroWeb/omeroweb/webclient/static/webclient/image/left_sidebar_icon_tags.png differ diff --git a/components/tools/OmeroWeb/omeroweb/webclient/static/webclient/image/left_sidebar_icon_tags_locked.png b/components/tools/OmeroWeb/omeroweb/webclient/static/webclient/image/left_sidebar_icon_tags_locked.png deleted file mode 100644 index e234a5cbea9..00000000000 Binary files a/components/tools/OmeroWeb/omeroweb/webclient/static/webclient/image/left_sidebar_icon_tags_locked.png and /dev/null differ diff --git a/components/tools/OmeroWeb/omeroweb/webclient/static/webclient/javascript/jquery.jstree.omecut_plugin.js b/components/tools/OmeroWeb/omeroweb/webclient/static/webclient/javascript/jquery.jstree.omecut_plugin.js index c334c481d05..18a87d74201 100644 --- a/components/tools/OmeroWeb/omeroweb/webclient/static/webclient/javascript/jquery.jstree.omecut_plugin.js +++ b/components/tools/OmeroWeb/omeroweb/webclient/static/webclient/javascript/jquery.jstree.omecut_plugin.js @@ -93,13 +93,15 @@ // Handle this as a separate each for now $.each(tmp, function(index, node) { var parent = inst.get_node(inst.get_parent(node)); - // remove node... - inst.delete_node(node); - - // Objects which were already orphaned require no action except to be added to the paste buffer + // Objects which were already orphaned (or parent is tag) require no action + // except to be added to the paste buffer if (parent.type !== 'experimenter' && - parent.type !== 'orphaned') { + parent.type !== 'orphaned' && + parent.type !== 'tag') { + + // remove node... + inst.delete_node(node); // Do the unlinking. Result will tell us whether object is orphaned // If orphaned, move object under 'orphaned' or 'experimenter' @@ -116,7 +118,9 @@ } } } - if (orphaned) { + // If cut obj is now an orphan (and we're not in the TAG_TREE)... + var wrongTree = (WEBCLIENT.TAG_TREE && (node.type === 'dataset' || node.type === 'image')); + if (orphaned && !wrongTree) { // Get the experimenter that owns this object // This handles the multi-experimenters shown case var ownerExperimenter = inst.locate_node('experimenter-' + activeUserId())[0], @@ -124,7 +128,7 @@ var new_node_data = inst._get_node_data(node); // Newly orphaned objects get moved to the appropriate location - if (node.type === 'dataset' || node.type === 'plate') { + if (node.type === 'dataset' || node.type === 'plate' || node.type === 'tag') { newParent = ownerExperimenter; } else if (node.type === 'image') { // Get the orphaned directory for this experimenter diff --git a/components/tools/OmeroWeb/omeroweb/webclient/static/webclient/javascript/ome.tree.js b/components/tools/OmeroWeb/omeroweb/webclient/static/webclient/javascript/ome.tree.js index 59645d9f7d9..3a90bfa29a8 100644 --- a/components/tools/OmeroWeb/omeroweb/webclient/static/webclient/javascript/ome.tree.js +++ b/components/tools/OmeroWeb/omeroweb/webclient/static/webclient/javascript/ome.tree.js @@ -366,9 +366,6 @@ $(function() { if (selected.length > 0 && selected[0].type !== node.type) { return false; } - if (selected.length > 0 && (node.type === 'tag' || node.type === 'tagset')) { - return false; - } // Also disallow the selection if it is a multi-select and the new target // is already selected @@ -744,7 +741,7 @@ $(function() { }, 'experimenter': { 'icon' : WEBCLIENT.URLS.static_webclient + 'image/icon_user.png', - 'valid_children': ['project','dataset','screen','plate'] + 'valid_children': ['project','dataset','screen','plate', 'tag', 'tagset'] }, 'tagset': { 'icon': WEBCLIENT.URLS.static_webclient + 'image/left_sidebar_icon_tags.png', @@ -752,7 +749,8 @@ $(function() { }, 'tag': { 'icon': WEBCLIENT.URLS.static_webclient + 'image/left_sidebar_icon_tag.png', - 'valid_children': ['project, dataset, image, screen, plate, acquisition'] + 'valid_children': ['project, dataset, image, screen, plate, acquisition'], + 'draggable': true }, 'project': { 'icon': WEBCLIENT.URLS.static_webclient + 'image/folder16.png', @@ -761,11 +759,11 @@ $(function() { 'dataset': { 'icon': WEBCLIENT.URLS.static_webclient + 'image/folder_image16.png', 'valid_children': ['image'], - 'draggable': true + 'draggable': !WEBCLIENT.TAG_TREE }, 'image': { 'icon': WEBCLIENT.URLS.static_webclient + 'image/image16.png', - 'draggable': true + 'draggable': !WEBCLIENT.TAG_TREE }, 'screen': { 'icon': WEBCLIENT.URLS.static_webclient + 'image/folder_screen16.png', @@ -774,7 +772,7 @@ $(function() { 'plate': { 'icon': WEBCLIENT.URLS.static_webclient + 'image/folder_plate16.png', 'valid_children': ['acquisition'], - 'draggable': true + 'draggable': !WEBCLIENT.TAG_TREE }, 'acquisition': { 'icon': WEBCLIENT.URLS.static_webclient + 'image/run16.png', @@ -790,9 +788,12 @@ $(function() { var inst = $.jstree.reference(nodes[0]); // Check if the node types are draggable and the particular nodes have the // 'canLink' permission. All must pass + // Don't allow dragging of any object from under a tag for (var index in nodes) { - if (!inst.get_rules(nodes[index]).draggable || - !OME.nodeHasPermission(nodes[index], 'canLink') + var node = nodes[index]; + if (!inst.get_rules(node).draggable || + !OME.nodeHasPermission(node, 'canLink') || + inst.get_node(node.parent).type === 'tag' ) { return false; } @@ -806,16 +807,29 @@ $(function() { 'items' : function(node){ var config = {}; - // Hack to disable context menu for tags tree - // Need to fix permissions using map() in api_tagged query first - if (WEBCLIENT.URLS.tree_top_level === WEBCLIENT.URLS.api_tags_and_tagged) { - return config; - } - config["create"] = { "label" : "Create new", "_disabled": true, - "submenu": { + }; + + var tagTree = (WEBCLIENT.URLS.tree_top_level === WEBCLIENT.URLS.api_tags_and_tagged); + if (tagTree) { + config["create"]["submenu"] = { + "tagset": { + "label" : "Tag Set", + "_disabled" : true, + "icon" : WEBCLIENT.URLS.static_webclient + 'image/left_sidebar_icon_tags.png', + action : function (node) {OME.handleNewContainer("tagset"); }, + }, + "tag": { + "label" : "Tag", + "_disabled" : true, + "icon" : WEBCLIENT.URLS.static_webclient + 'image/left_sidebar_icon_tag.png', + action : function (node) {OME.handleNewContainer("tag"); }, + } + }; + } else { + config["create"]["submenu"] = { "project": { "label" : "Project", "_disabled": true, @@ -834,8 +848,8 @@ $(function() { "icon" : WEBCLIENT.URLS.static_webclient + 'image/folder_screen16.png', action: function (node) {OME.handleNewContainer("screen"); }, } - } - }; + }; + } config["ccp"] = { "label" : "Edit", @@ -965,9 +979,26 @@ $(function() { // List of permissions related disabling // use canLink, canDelete etc classes on each node to enable/disable right-click menu - // TODO Potentially #8879 needs to be handled either by disabling all subnodes or by never - // creating them. As the menu is created anew each time there is no reason not to never create - // those nodes + var userId = WEBCLIENT.active_user_id, + canCreate = (userId === WEBCLIENT.USER.id || userId === -1), + canLink = OME.nodeHasPermission(node, 'canLink'), + parentAllowsCreate = (node.type === "orphaned" || node.type === "experimenter"); + + + // Although not everything created here will go under selected node, + // we still don't allow creation if linking not allowed + if(canCreate && (canLink || parentAllowsCreate)) { + // Enable tag or P/D/I submenus created above + config["create"]["_disabled"] = false; + if (tagTree) { + config["create"]["submenu"]["tagset"]["_disabled"] = false; + config["create"]["submenu"]["tag"]["_disabled"] = false; + } else { + config["create"]["submenu"]["project"]["_disabled"] = false; + config["create"]["submenu"]["dataset"]["_disabled"] = false; + config["create"]["submenu"]["screen"]["_disabled"] = false; + } + } // Disable delete if no canDelete permission if (OME.nodeHasPermission(node, 'canDelete')) { @@ -976,83 +1007,50 @@ $(function() { // Enable 'Move to group' if 'canChgrp' if(OME.nodeHasPermission(node, 'canChgrp')) { - // Can chgrp everything except Plate 'run' - if (node.type !== "acquisition") { + // Can chgrp everything except Plate 'run', 'tag' and 'tagset' + if (["acquisition", "tag", "tagset"].indexOf(node.type) === -1) { config["chgrp"]["_disabled"] = false; } } - // The only cases where 'Create' menu depends on selected node are - // Project & Dataset (see below). For all others we can enable 'Create'... - if (node.type !== 'project' && node.type !== 'dataset') { - - var userId = WEBCLIENT.USER.id, - canCreate = (userId === WEBCLIENT.USER.id || userId === -1); - - if(canCreate) { - config["create"]["_disabled"] = false; - config["create"]["submenu"]["project"]["_disabled"] = false; - config["create"]["submenu"]["dataset"]["_disabled"] = false; - config["create"]["submenu"]["screen"]["_disabled"] = false; - } - } - if (OME.nodeHasPermission(node, 'canLink')) { + if (canLink) { var to_paste = false, buffer = this.get_buffer(), - parent_id = node.parent; + parent_id = node.parent, + parent_type = this.get_node(parent_id).type, + node_type = node.type; if(this.can_paste() && buffer.node) { to_paste = buffer.node[0].type; } - // If canLink, handle other node types... - if (node.type === "project") { - // Project: can create Dataset - config["create"]["_disabled"] = false; - config["create"]["submenu"]["dataset"]["_disabled"] = false; - // if we have a dataset, allow paste - if (to_paste === "dataset") { - config["ccp"]["_disabled"] = false; - config["ccp"]["submenu"]["paste"]["_disabled"] = false; - } - } else if(node.type === "dataset") { - // Dataset, allow cut - config["ccp"]["_disabled"] = false; - config["ccp"]["submenu"]["cut"]["_disabled"] = false; - // If project parent, allow copy. - if (this.get_node(parent_id).type === 'project') { - config["ccp"]["submenu"]["copy"]["_disabled"] = false; - } - // we have an image, allow paste - if (to_paste === "image") { - config["ccp"]["_disabled"] = false; - config["ccp"]["submenu"]["paste"]["_disabled"] = false; - } - } else if (node.type === "image") { - // Image, allow cut - config["ccp"]["_disabled"] = false; - config["ccp"]["submenu"]["cut"]["_disabled"] = false; - // If dataset parent, allow copy. - if (this.get_node(parent_id).type === 'dataset') { - config["ccp"]["submenu"]["copy"]["_disabled"] = false; - } - // allow 'share' - config["share"]["_disabled"] = false; - } else if (node.type === "screen") { - // Screen: we have a Plate, allow paste - if (to_paste === "plate") { - config["ccp"]["_disabled"] = false; - config["ccp"]["submenu"]["paste"]["_disabled"] = false; - } - } else if (node.type === "plate") { - // Plate, allow cut + + // Currently we allow to Cut, even if we don't delete parent link! + // E.g. can Cut orphaned Image or orphaned Dataset. TODO: review this! + var canCut = (["dataset", "image", "plate", "tag"].indexOf(node_type) > -1); + // In Tag tree. Don't allow cut under tag + if (tagTree && node_type !== "tag") { + canCut = false; + } + + // Currently we only allow Copy if parent is compatible?! TODO: review this! + var canCopy = ((node_type === "dataset" && parent_type === "project") || + (node_type === "image" && parent_type === "dataset") || + (node_type === "plate" && parent_type === "screen") || + (node_type === "tag" && parent_type === "tagset")); + // In Tag tree, can't Copy anything except tag + if (tagTree && node_type !== "tag"){ + canCopy = false; + } + + var canPaste = ((node_type === "project" && to_paste === "dataset") || + (node_type === "dataset" && to_paste === "image") || + (node_type === "screen" && to_paste === "plate") || + (node_type === "tagset" && to_paste === "tag")); + if (canCut || canCopy || canPaste){ config["ccp"]["_disabled"] = false; - config["ccp"]["submenu"]["cut"]["_disabled"] = false; - // If Screen parent, allow copy. - if (this.get_node(parent_id).type === 'screen') { - config["ccp"]["submenu"]["copy"]["_disabled"] = false; - } - } else if (node.type === "acquisition") { - // nothing else needs to be enabled + config["ccp"]["submenu"]["cut"]["_disabled"] = !canCut; + config["ccp"]["submenu"]["copy"]["_disabled"] = !canCopy; + config["ccp"]["submenu"]["paste"]["_disabled"] = !canPaste; } } @@ -1102,8 +1100,12 @@ $(function() { return 6; } else if (node.type === 'orphaned') { return 7; - } else { + } else if (node.type === 'image') { return 8; + } else if (node.type === 'acquisition') { + return 9; + } else { + return 10; } } // If the nodes are the same type then just compare lexicographically diff --git a/components/tools/OmeroWeb/omeroweb/webclient/static/webclient/javascript/ome.webclient.actions.js b/components/tools/OmeroWeb/omeroweb/webclient/static/webclient/javascript/ome.webclient.actions.js index 01776087c45..21a0241ccc8 100644 --- a/components/tools/OmeroWeb/omeroweb/webclient/static/webclient/javascript/ome.webclient.actions.js +++ b/components/tools/OmeroWeb/omeroweb/webclient/static/webclient/javascript/ome.webclient.actions.js @@ -454,7 +454,7 @@ OME.truncateNames = (function(){ return truncateNames; }()); -// Handle deletion of selected objects in jsTree in container_tags.html and containers.html +// Handle deletion of selected objects in jsTree in containers.html OME.handleDelete = function(deleteUrl, filesetCheckUrl, userId) { var datatree = $.jstree.reference($('#dataTree')); var selected = datatree.get_selected(true); @@ -492,8 +492,6 @@ OME.handleDelete = function(deleteUrl, filesetCheckUrl, userId) { } var notOwned = false; $.each(selected, function(index, node) { - // Add the nodes that are to be deleted - ajax_data.push(node.type + '=' + node.data.obj.id); // What types are being deleted and how many (for pluralization) var dtype = node.type; if (dtype in dtypes) { @@ -501,6 +499,8 @@ OME.handleDelete = function(deleteUrl, filesetCheckUrl, userId) { } else { dtypes[dtype] = 1; } + // Add the nodes that are to be deleted + ajax_data.push(dtype.replace('tagset', 'tag') + '=' + node.data.obj.id); // If the node type is not 'image' then ask about deleting contents if (!askDeleteContents && node.type != 'image') { askDeleteContents = true; @@ -550,6 +550,20 @@ OME.handleDelete = function(deleteUrl, filesetCheckUrl, userId) { type: "POST", success: function(r){ + // If we've deleted Tagset, child Tags should appear as orphans in tree + // Before deleting, copy data from each child, to add back below... + var child_tags = []; + if (dtypes["tagset"]) { + selected.forEach(function(node){ + node.children.forEach(function(ch) { + ch = datatree.get_node(ch); + // _get_node_data is provided by the omecut_plugin + var d = datatree._get_node_data(ch); + child_tags.push(d); + }); + }); + } + datatree.delete_node(selected); // Update the central panel with new selection @@ -558,11 +572,30 @@ OME.handleDelete = function(deleteUrl, filesetCheckUrl, userId) { if (firstParent.type !== "plate") { datatree.select_node(firstParent); } - $.each(disabledNodes, function(index, node) { - //TODO Make use of server calculated update like chgrp? - updateParentRemoveNode(datatree, node, firstParent); - removeDuplicateNodes(datatree, node); - }); + + // Here we try to handle children of the deleted object. + // In case we deleted a "tagset", child tags should be kept as orphans under experimenter + // (unless they are found under other tag sets). + // For other objects we remove any duplicates of the object + // (E.g if "dataset" is deleted and appears in tree multiple times) + // In both cases we can only work with loaded data - Don't know if 'tag' or 'dataset' + // is under unloaded 'tagset' or 'project'. + // Would need to get this info from server as we do with 'Cut' + if (dtypes["tagset"]) { + // Re-create child tags under experimenter parent + child_tags.forEach(function(d){ + var nodeId = d.type + '-' + d.data.obj.id; + if (!datatree.locate_node(nodeId, firstParent)) { + datatree.create_node(firstParent, d); + } + }); + } else { + // Remove duplicates of the deleted object + $.each(disabledNodes, function(index, node) { + updateParentRemoveNode(datatree, node, firstParent); + removeDuplicateNodes(datatree, node); + }); + } // Update the central panel in case delete has removed an icon $.each(selected, function(index, node) { diff --git a/components/tools/OmeroWeb/omeroweb/webclient/templates/webclient/annotations/includes/toolbar.html b/components/tools/OmeroWeb/omeroweb/webclient/templates/webclient/annotations/includes/toolbar.html index 683735e31c4..2e65d80f1cd 100644 --- a/components/tools/OmeroWeb/omeroweb/webclient/templates/webclient/annotations/includes/toolbar.html +++ b/components/tools/OmeroWeb/omeroweb/webclient/templates/webclient/annotations/includes/toolbar.html @@ -269,17 +269,19 @@ {% if image %} - + {% endif %} diff --git a/components/tools/OmeroWeb/omeroweb/webclient/templates/webclient/annotations/metadata_preview.html b/components/tools/OmeroWeb/omeroweb/webclient/templates/webclient/annotations/metadata_preview.html index 56e57ca5548..663d09c2f83 100644 --- a/components/tools/OmeroWeb/omeroweb/webclient/templates/webclient/annotations/metadata_preview.html +++ b/components/tools/OmeroWeb/omeroweb/webclient/templates/webclient/annotations/metadata_preview.html @@ -329,10 +329,9 @@ {% endif %} - $("#preview_open_viewer").click(function(){ - - var url = "{% if share and not share.share.isOwned %}{% url 'web_image_viewer' share.share.id manager.image.id %}{% else %}{% url 'web_image_viewer' manager.image.id %}{% endif %}"; - + $("#preview_open_viewer").click(function(event){ + event.preventDefault(); + var url = $(this).attr('href'); var vpQuery = OME.preview_viewport.getQuery(); // remove &zm=50 vpQuery = vpQuery.replace("&zm=" + OME.preview_viewport.getZoom(), ""); @@ -351,11 +350,14 @@ - +

diff --git a/components/tools/OmeroWeb/omeroweb/webclient/templates/webclient/data/containers.html b/components/tools/OmeroWeb/omeroweb/webclient/templates/webclient/data/containers.html index 69a10b4e537..d46ff4f2637 100644 --- a/components/tools/OmeroWeb/omeroweb/webclient/templates/webclient/data/containers.html +++ b/components/tools/OmeroWeb/omeroweb/webclient/templates/webclient/data/containers.html @@ -60,6 +60,7 @@ WEBCLIENT.active_group_id = {{ active_group.id }}; WEBCLIENT.USER = {'id': {{ ome.user.id }} }; + WEBCLIENT.active_user_id = {{ ome.user_id }}; WEBCLIENT.URLS = {}; WEBCLIENT.URLS.webindex = "{% url 'webindex' %}"; @@ -78,6 +79,7 @@ WEBCLIENT.URLS.reset_rdef_json = "{% url 'reset_rdef_json' %}"; {% ifequal menu 'usertags' %} + WEBCLIENT.TAG_TREE = true; WEBCLIENT.URLS.tree_top_level = WEBCLIENT.URLS.api_tags_and_tagged; {% else %} WEBCLIENT.URLS.tree_top_level = WEBCLIENT.URLS.api_containers; @@ -92,21 +94,8 @@ // Variable to store selection data when using jstree refresh var refreshPathsReverse = []; - // Variable to enable resetting of the reload central panel timeout - // var centreReloadTimeout = false; - // function centreReload(event, data) { - - // if (centreReloadTimeout) { - // window.clearTimeout(centreReloadTimeout); - // } - - // centreReloadTimeout = window.setTimeout(function() { - // // Update the central panel - // update_thumbnails_panel(event, data); - // centreReloadTimeout = false; - // }, 200); - // } + // Called from ome.tree.js var updateParentRemoveNode = function(inst, node, parent) { /* Update any other instances of the parent of this node to remove it * Also Based on if the parent of this node has any children @@ -293,6 +282,8 @@ "addproject":false, 'adddataset':false, 'addscreen':false, + 'addtag': false, + 'addtagset': false, 'copy':false, 'cut':false, 'paste': false, @@ -304,11 +295,20 @@ var userId = {{ ome.user_id }}, canCreate = (userId === {{ ome.user.id }} || userId === -1); + // Also need all selected items to allow linking, + // because new objects are sometimes linked under selected items + canCreate = selected.reduce(function(prev, n){ + var canLnk = (n.type === 'experimenter' || n.type === 'orphaned' || OME.nodeHasPermission(n, 'canLink')); + return canLnk && prev; + }, canCreate); + // These nodes can be Orphans, so creation is not selection-specific if (canCreate) { toolbar_config["addproject"] = true; toolbar_config["adddataset"] = true; toolbar_config['addscreen'] = true; + toolbar_config["addtag"] = true; + toolbar_config["addtagset"] = true; } if(selected.length > 0) { @@ -339,18 +339,31 @@ return false; } }); - } - // Only allow copy/cut if the selected item(s) are elligible. This uses the slightly + // Only allow cut if the selected item(s) are elligible. This uses the slightly // confusingly named 'is_draggable' which is part of the drag'n'drop plugin // which in turn uses a jstree node type property 'draggable' // It also checks it the selected nodes can be linked if(inst.settings.dnd.is_draggable(selected)) { - toolbar_config['copy'] = true; toolbar_config['cut'] = true; } + // Allow Copy of Dataset/Image/Plate if you 'canLink' all selected nodes + var canCopy = selected.reduce(function(prev, n){ + var node_type = n.type, + parent_type = inst.get_node(n.parent).type; + // In tag tree, can't copy anything except a tag + var invalidType = (WEBCLIENT.TAG_TREE && node_type !== "tag"); + // Can Copy objects under their true parent types (NOT orphaned tag, dataset etc) + var plink = ((node_type === "dataset" && parent_type === "project") || + (node_type === "image" && parent_type === "dataset") || + (node_type === "plate" && parent_type === "screen") || + (node_type === "tag" && parent_type === "tagset")); + return (!invalidType) && plink && OME.nodeHasPermission(n, 'canLink') && prev; + }, true); + toolbar_config['copy'] = canCopy; + // Only images can be added to a basket and only if they all are toolbar_config['createshare'] = true; $.each(selected, function(index, node) { @@ -465,6 +478,7 @@ // Remove duplicate nodes, normally as a result of copy_node // or move_node + // Global function, called from omecut_plugin function removeDuplicate(inst, node, parentId) { var parent = inst.get_node(parentId), found = false; @@ -543,16 +557,42 @@ } }); - // If a project is selected, create dataset under it + // Default: Create an orphan of "folder_type" ('project', 'dataset', 'screen', 'tag', 'tagset' etc. ) + url = '{% url 'manage_action_containers' "addnewcontainer" %}'; + // Find the 'experimenter' node as parent + var root = inst.get_node('#'); + $.each(root.children, function(index, id) { + var node = inst.get_node(id); + if (node.type === 'experimenter' && node.data.obj.id === {{ ome.user_id }}) { + parent = node; + // Break out of each + return false; + } + }); + + // If a project is selected (or selected is a child of project) create dataset under it var url, position = 0; var parent = false; - if(selected.length == 1 && selected[0].type === 'project' && cont_type == 'dataset') { - url = '{% url 'manage_action_containers' "addnewcontainer" %}project/'+selected[0].data.obj.id+'/'; - parent = selected[0]; - // otherwise create an orphan of "folder_type" ('project', 'dataset', 'screen' etc. ) + if(selected.length == 1 && cont_type == 'dataset') { + if (selected[0].type === 'project') { + parent = selected[0]; + } else if (inst.get_node(selected[0].parent).type === 'project') { + parent = inst.get_node(selected[0].parent); + } + // If a tagset is selected (or selected is a child of tagset), create tag under it + } else if(selected.length == 1 && cont_type == 'tag') { + if (selected[0].type === 'tagset') { + parent = selected[0]; + } else if (inst.get_node(selected[0].parent).type === 'tagset') { + parent = inst.get_node(selected[0].parent); + } + } + if (parent) { + url = '{% url 'manage_action_containers' "addnewcontainer" %}' + parent.type + '/' + parent.data.obj.id + '/'; } else { + // otherwise create an orphan of "folder_type" ('project', 'dataset', 'screen', 'tag', 'tagset' etc. ) url = '{% url 'manage_action_containers' "addnewcontainer" %}'; - // Make sure top level objects get added to jsTree root (current selected may be project, dataset, image or screen) + // Find 'experimenter' to be parent var root = inst.get_node('#'); $.each(root.children, function(index, id) { var node = inst.get_node(id); @@ -658,6 +698,14 @@ OME.handleNewContainer("screen"); }); + $('#addtagButton').click(function() { + OME.handleNewContainer("tag"); + }); + + $('#addtagsetButton').click(function() { + OME.handleNewContainer("tagset"); + }); + $('#copyButton').click(function() { var objs = inst.get_selected(true) inst.copy(objs); @@ -753,23 +801,21 @@
- {% ifequal menu 'usertags' %} - + + {% ifequal menu 'usertags' %} +
  • +
  • {% else %} - - - {% endifequal %}
    diff --git a/components/tools/OmeroWeb/omeroweb/webclient/templates/webclient/data/icon_thumbnails_underscore.html b/components/tools/OmeroWeb/omeroweb/webclient/templates/webclient/data/icon_thumbnails_underscore.html index f442c43ac50..bc18aebac84 100644 --- a/components/tools/OmeroWeb/omeroweb/webclient/templates/webclient/data/icon_thumbnails_underscore.html +++ b/components/tools/OmeroWeb/omeroweb/webclient/templates/webclient/data/icon_thumbnails_underscore.html @@ -24,9 +24,12 @@ data-owned="">
    - image + + + image +
    diff --git a/components/tools/OmeroWeb/omeroweb/webclient/templates/webclient/data/includes/center_plugin.thumbs.js.html b/components/tools/OmeroWeb/omeroweb/webclient/templates/webclient/data/includes/center_plugin.thumbs.js.html index 981096bf989..5fed97b4403 100644 --- a/components/tools/OmeroWeb/omeroweb/webclient/templates/webclient/data/includes/center_plugin.thumbs.js.html +++ b/components/tools/OmeroWeb/omeroweb/webclient/templates/webclient/data/includes/center_plugin.thumbs.js.html @@ -106,6 +106,7 @@ // single click handler on image (container). Selection then update toolbar & metadata pane $( "#icon_table" ).on( "click", "li.row", function(event) { + event.preventDefault(); handleClickSelection(event); }); @@ -399,6 +400,8 @@ if (event.target.nodeName.toLowerCase() == 'li') { $targetIcon = $(event.target); } else if (event.target.nodeName.toLowerCase() == 'img') { + $targetIcon = $(event.target).parent().parent().parent(); + } else if (event.target.nodeName.toLowerCase() == 'a') { $targetIcon = $(event.target).parent().parent(); } else { $targetIcon = $(event.target).parent(); diff --git a/components/tools/OmeroWeb/omeroweb/webclient/views.py b/components/tools/OmeroWeb/omeroweb/webclient/views.py index 8df7c07bd9d..2e8ea170c43 100755 --- a/components/tools/OmeroWeb/omeroweb/webclient/views.py +++ b/components/tools/OmeroWeb/omeroweb/webclient/views.py @@ -93,7 +93,7 @@ from omero.model import ProjectI, DatasetI, ImageI, \ ScreenI, PlateI, \ ProjectDatasetLinkI, DatasetImageLinkI, \ - ScreenPlateLinkI + ScreenPlateLinkI, AnnotationAnnotationLinkI, TagAnnotationI from omero import ApiUsageException, ServerError, CmdError from omero.rtypes import rlong, rlist @@ -815,7 +815,7 @@ def get_object_links(conn, parent_type, parent_id, child_type, child_ids): return None link_type = None if parent_type == 'experimenter': - if child_type == 'dataset' or child_type == 'plate': + if child_type in ['dataset', 'plate', 'tag']: # This will be a requested link if a dataset or plate is # moved from the de facto orphaned datasets/plates, it isn't # an error, but no link actually needs removing @@ -829,7 +829,9 @@ def get_object_links(conn, parent_type, parent_id, child_type, child_ids): elif parent_type == 'screen': if child_type == 'plate': link_type = 'ScreenPlateLink' - + elif parent_type == 'tagset': + if child_type == 'tag': + link_type = 'AnnotationAnnotationLink' if not link_type: raise Http404("json data needs 'parent_type' and 'child_type'") @@ -837,8 +839,10 @@ def get_object_links(conn, parent_type, parent_id, child_type, child_ids): params.addIds(child_ids) qs = conn.getQueryService() + # Need to fetch child and parent, otherwise + # AnnotationAnnotationLink is not loaded q = """ - from %s olink + from %s olink join fetch olink.child join fetch olink.parent where olink.child.id in (:ids) """ % link_type if parent_id: @@ -853,53 +857,72 @@ def get_object_links(conn, parent_type, parent_id, child_type, child_ids): return link_type, res +def create_link(parent_type, parent_id, child_type, child_id): + """ This is just used internally by api_link DELETE below """ + if parent_type == 'experimenter': + if child_type == 'dataset' or child_type == 'plate': + # This is actually not a link that needs creating, this + # dataset/plate is an orphan + return 'orphan' + if parent_type == 'project': + project = ProjectI(long(parent_id), False) + if child_type == 'dataset': + dataset = DatasetI(long(child_id), False) + l = ProjectDatasetLinkI() + l.setParent(project) + l.setChild(dataset) + return l + elif parent_type == 'dataset': + dataset = DatasetI(long(parent_id), False) + if child_type == 'image': + image = ImageI(long(child_id), False) + l = DatasetImageLinkI() + l.setParent(dataset) + l.setChild(image) + return l + elif parent_type == 'screen': + screen = ScreenI(long(parent_id), False) + if child_type == 'plate': + plate = PlateI(long(child_id), False) + l = ScreenPlateLinkI() + l.setParent(screen) + l.setChild(plate) + return l + elif parent_type == 'tagset': + if child_type == 'tag': + l = AnnotationAnnotationLinkI() + l.setParent(TagAnnotationI(long(parent_id), False)) + l.setChild(TagAnnotationI(long(child_id), False)) + return l + return None + + @login_required() def api_links(request, conn=None, **kwargs): - """ Creates or Deletes links between objects specified by a json + """ + Entry point for the api_links methods. + We delegate depending on request method to + create or delete links between objects. + """ + # Handle link creation/deletion + json_data = json.loads(request.body) + + if request.method == 'POST': + return _api_links_POST(conn, json_data) + elif request.method == 'DELETE': + return _api_links_DELETE(conn, json_data) + + +def _api_links_POST(conn, json_data, **kwargs): + """ Creates links between objects specified by a json blob in the request body. e.g. {"dataset":{"10":{"image":[1,2,3]}}} When creating a link, fails silently if ValidationException (E.g. adding an image to a Dataset that already has that image). """ - def create_link(parent_type, parent_id, child_type, child_id): - # TODO Handle more types of link - if parent_type == 'experimenter': - if child_type == 'dataset' or child_type == 'plate': - # This is actually not a link that needs creating, this - # dataset/plate is an orphan - return 'orphan' - if parent_type == 'project': - project = ProjectI(long(parent_id), False) - if child_type == 'dataset': - dataset = DatasetI(long(child_id), False) - l = ProjectDatasetLinkI() - l.setParent(project) - l.setChild(dataset) - return l - elif parent_type == 'dataset': - dataset = DatasetI(long(parent_id), False) - if child_type == 'image': - image = ImageI(long(child_id), False) - l = DatasetImageLinkI() - l.setParent(dataset) - l.setChild(image) - return l - elif parent_type == 'screen': - screen = ScreenI(long(parent_id), False) - if child_type == 'plate': - plate = PlateI(long(child_id), False) - l = ScreenPlateLinkI() - l.setParent(screen) - l.setChild(plate) - return l - return None - response = {'success': False} - # Handle link creation/deletion - json_data = json.loads(request.body) - # json is [parent_type][parent_id][child_type][childIds] # e.g. {"dataset":{"10":{"image":[1,2,3]}}} @@ -909,48 +932,19 @@ def create_link(parent_type, parent_id, child_type, child_id): continue for parent_id, children in parents.items(): for child_type, child_ids in children.items(): - if request.method == 'DELETE': - objLnks = get_object_links(conn, parent_type, - parent_id, - child_type, - child_ids) - if objLnks is None: - continue - linkType, links = objLnks - linkIds = [r.id.val for r in links] - logger.info("api_link: Deleting %s links" % len(linkIds)) - conn.deleteObjects(linkType, linkIds) - # webclient needs to know what is orphaned - linkType, remainingLinks = get_object_links(conn, - parent_type, - None, - child_type, - child_ids) - # return remaining links in same format as json above - # e.g. {"dataset":{"10":{"image":[1,2,3]}}} - for rl in remainingLinks: - pid = rl.parent.id.val - cid = rl.child.id.val - # Deleting links still in progress above - ignore these - if pid == int(parent_id): - continue - if parent_type not in response: - response[parent_type] = {} - if pid not in response[parent_type]: - response[parent_type][pid] = {child_type: []} - response[parent_type][pid][child_type].append(cid) - - elif request.method == 'POST': - for child_id in child_ids: - parent_id = int(parent_id) - link = create_link(parent_type, parent_id, - child_type, child_id) - if link and link != 'orphan': - linksToSave.append(link) - - if request.method == 'POST' and len(linksToSave) > 0: + for child_id in child_ids: + parent_id = int(parent_id) + link = create_link(parent_type, parent_id, + child_type, child_id) + if link and link != 'orphan': + linksToSave.append(link) + + if len(linksToSave) > 0: # Need to set context to correct group (E.g parent group) - p = conn.getQueryService().get(parent_type.title(), parent_id, + ptype = parent_type.title() + if ptype in ["Tagset", "Tag"]: + ptype = "TagAnnotation" + p = conn.getQueryService().get(ptype, parent_id, conn.SERVICE_OPTS) conn.SERVICE_OPTS.setOmeroGroup(p.details.group.id.val) logger.info("api_link: Saving %s links" % len(linksToSave)) @@ -971,12 +965,57 @@ def create_link(parent_type, parent_id, child_type, child_id): pass response['success'] = True - elif request.method == 'DELETE': - # If we got here, DELETE was OK - response['success'] = True + return HttpJsonResponse(response) + + +def _api_links_DELETE(conn, json_data): + """ Deletes links between objects specified by a json + blob in the request body. + e.g. {"dataset":{"10":{"image":[1,2,3]}}} + """ + + response = {'success': False} + + # json is [parent_type][parent_id][child_type][childIds] + # e.g. {"dataset":{"10":{"image":[1,2,3]}}} + for parent_type, parents in json_data.items(): + if parent_type == "orphaned": + continue + for parent_id, children in parents.items(): + for child_type, child_ids in children.items(): + objLnks = get_object_links(conn, parent_type, + parent_id, + child_type, + child_ids) + if objLnks is None: + continue + linkType, links = objLnks + linkIds = [r.id.val for r in links] + logger.info("api_link: Deleting %s links" % len(linkIds)) + conn.deleteObjects(linkType, linkIds) + # webclient needs to know what is orphaned + linkType, remainingLinks = get_object_links(conn, + parent_type, + None, + child_type, + child_ids) + # return remaining links in same format as json above + # e.g. {"dataset":{"10":{"image":[1,2,3]}}} + for rl in remainingLinks: + pid = rl.parent.id.val + cid = rl.child.id.val + # Deleting links still in progress above - ignore these + if pid == int(parent_id): + continue + if parent_type not in response: + response[parent_type] = {} + if pid not in response[parent_type]: + response[parent_type][pid] = {child_type: []} + response[parent_type][pid][child_type].append(cid) + + # If we got here, DELETE was OK + response['success'] = True - # Currently we don't use the response for anything. - # Could return more info in future if useful? return HttpJsonResponse(response) @@ -2019,7 +2058,13 @@ def batch_annotate(request, conn=None, **kwargs): obj_string = "&".join(obj_ids) link_string = "|".join(obj_ids).replace("=", "-") if len(groupIds) == 0: - return handlerInternalError(request, "No objects found") + # No supported objects found. + # If multiple tags / tagsets selected, return placeholder + if (len(request.GET.getlist('tag')) > 0 or + len(request.GET.getlist('tagset')) > 0): + return HttpResponse("

    Can't batch annotate tags

    ") + else: + return handlerInternalError(request, "No objects found") groupId = list(groupIds)[0] conn.SERVICE_OPTS.setOmeroGroup(groupId) @@ -2599,9 +2644,6 @@ def manage_action_containers(request, action, o_type=None, o_id=None, manager = None if o_type in ("dataset", "project", "image", "screen", "plate", "acquisition", "well", "comment", "file", "tag", "tagset"): - if o_type == 'tagset': - # TODO: this should be handled by the BaseContainer - o_type = 'tag' kw = {'index': index} if o_type is not None and o_id > 0: kw[str(o_type)] = long(o_id) @@ -2616,13 +2658,13 @@ def manage_action_containers(request, action, o_type=None, o_id=None, form = None if action == 'addnewcontainer': - # Used within the jsTree to add a new Project, Dataset etc under a - # specified parent OR top-level + # Used within the jsTree to add a new Project, Dataset, Tag, + # Tagset etc under a specified parent OR top-level if not request.method == 'POST': return HttpResponseRedirect(reverse("manage_action_containers", args=["edit", o_type, o_id])) - if o_type is not None and hasattr(manager, o_type) and o_id > 0: - # E.g. Parent o_type is 'project'... + if o_type == "project" and hasattr(manager, o_type) and o_id > 0: + # If Parent o_type is 'project'... form = ContainerForm(data=request.POST.copy()) if form.is_valid(): logger.debug( @@ -2638,8 +2680,22 @@ def manage_action_containers(request, action, o_type=None, o_id=None, d.update({e[0]: unicode(e[1])}) rdict = {'bad': 'true', 'errs': d} return HttpJsonResponse(rdict) + elif o_type == "tagset" and o_id > 0: + form = ContainerForm(data=request.POST.copy()) + if form.is_valid(): + name = form.cleaned_data['name'] + description = form.cleaned_data['description'] + oid = manager.createTag(name, description) + rdict = {'bad': 'false', 'id': oid} + return HttpJsonResponse(rdict) + else: + d = dict() + for e in form.errors.iteritems(): + d.update({e[0]: unicode(e[1])}) + rdict = {'bad': 'true', 'errs': d} + return HttpJsonResponse(rdict) elif request.POST.get('folder_type') in ("project", "screen", - "dataset"): + "dataset", "tag", "tagset"): # No parent specified. We can create orphaned 'project', 'dataset' # etc. form = ContainerForm(data=request.POST.copy()) @@ -2653,6 +2709,7 @@ def manage_action_containers(request, action, o_type=None, o_id=None, name, description, img_ids=request.POST.getlist('image', None)) else: + # lookup method, E.g. createTag, createProject etc. oid = getattr(manager, "create" + folder_type.capitalize())(name, description) rdict = {'bad': 'false', 'id': oid} diff --git a/components/tools/OmeroWeb/omeroweb/webclient/webclient_gateway.py b/components/tools/OmeroWeb/omeroweb/webclient/webclient_gateway.py index 950300d28ca..f6f0dfb51b3 100644 --- a/components/tools/OmeroWeb/omeroweb/webclient/webclient_gateway.py +++ b/components/tools/OmeroWeb/omeroweb/webclient/webclient_gateway.py @@ -638,6 +638,25 @@ def createScreen(self, name, description=None): sc.description = rstring(str(description)) return self.saveAndReturnId(sc) + def createTag(self, name, description=None): + """ Creates new Tag and returns ID """ + + tag = omero.model.TagAnnotationI() + tag.textValue = rstring(str(name)) + if description is not None and description != "": + tag.description = rstring(str(description)) + return self.saveAndReturnId(tag) + + def createTagset(self, name, description=None): + """ Creates new Tag Set and returns ID """ + + tag = omero.model.TagAnnotationI() + tag.textValue = rstring(str(name)) + tag.ns = rstring(omero.constants.metadata.NSINSIGHTTAGSET) + if description is not None and description != "": + tag.description = rstring(str(description)) + return self.saveAndReturnId(tag) + def createContainer(self, dtype, name, description=None): """ Creates new Project, Dataset or Screen and returns ID """ diff --git a/components/tools/OmeroWeb/omeroweb/webgateway/static/webgateway/css/ome.header.css b/components/tools/OmeroWeb/omeroweb/webgateway/static/webgateway/css/ome.header.css index e80155ad756..2e0f8c6eea2 100644 --- a/components/tools/OmeroWeb/omeroweb/webgateway/static/webgateway/css/ome.header.css +++ b/components/tools/OmeroWeb/omeroweb/webgateway/static/webgateway/css/ome.header.css @@ -297,6 +297,8 @@ max-height: 500px; min-width: 300px !important; left:-90px; + overflow-y: auto; + overflow-x: hidden; display: none; } diff --git a/components/tools/OmeroWeb/test/integration/test_containers.py b/components/tools/OmeroWeb/test/integration/test_containers.py new file mode 100644 index 00000000000..edfe8b74170 --- /dev/null +++ b/components/tools/OmeroWeb/test/integration/test_containers.py @@ -0,0 +1,182 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# Copyright (C) 2016 University of Dundee & Open Microscopy Environment. +# All rights reserved. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +""" +Tests creation, linking, editing & deletion of containers +""" + +import omero +import omero.clients +from omero.rtypes import rtime +from weblibrary import IWebTest +from weblibrary import _csrf_post_response, _post_response +from weblibrary import _csrf_delete_response, _delete_response + +import json + +from django.core.urlresolvers import reverse + + +class TestContainers(IWebTest): + """ + Tests creation, linking, editing & deletion of containers + """ + + def blank_image(self): + """ + Returns a new foundational Image with Channel objects attached for + view method testing. + """ + print dir(self) + + pixels = self.pix(client=self.client) + for the_c in range(pixels.getSizeC().val): + channel = omero.model.ChannelI() + channel.logicalChannel = omero.model.LogicalChannelI() + pixels.addChannel(channel) + image = pixels.getImage() + return self.sf.getUpdateService().saveAndReturnObject(image) + + def test_add_and_rename_container(self): + + # Add project + request_url = reverse("manage_action_containers", + args=["addnewcontainer"]) + data = { + 'folder_type': 'project', + 'name': 'foobar' + } + _post_response(self.django_client, request_url, data) + response = _csrf_post_response(self.django_client, request_url, data) + pid = json.loads(response.content).get("id") + + # Add dataset to the project + request_url = reverse("manage_action_containers", + args=["addnewcontainer", "project", pid]) + data = { + 'folder_type': 'dataset', + 'name': 'foobar' + } + _post_response(self.django_client, request_url, data) + _csrf_post_response(self.django_client, request_url, data) + + # Rename project + request_url = reverse("manage_action_containers", + args=["savename", "project", pid]) + data = { + 'name': 'anotherfoobar' + } + _post_response(self.django_client, request_url, data) + _csrf_post_response(self.django_client, request_url, data) + + # Change project description + request_url = reverse("manage_action_containers", + args=["savedescription", "project", pid]) + data = { + 'description': 'anotherfoobar' + } + _post_response(self.django_client, request_url, data) + _csrf_post_response(self.django_client, request_url, data) + + def test_paste_move_remove_deletamany_image(self): + + # Add dataset + request_url = reverse("manage_action_containers", + args=["addnewcontainer"]) + data = { + 'folder_type': 'dataset', + 'name': 'foobar' + } + _post_response(self.django_client, request_url, data) + response = _csrf_post_response(self.django_client, request_url, data) + did = json.loads(response.content).get("id") + + img = self.make_image() + print img + + # Link image to Dataset + request_url = reverse("api_links") + data = { + 'dataset': {did: {'image': [img.id.val]}} + } + + _post_response(self.django_client, request_url, data) + _csrf_post_response(self.django_client, + request_url, + json.dumps(data), + content_type="application/json") + + # Unlink image from Dataset + request_url = reverse("api_links") + data = { + 'dataset': {did: {'image': [img.id.val]}} + } + _delete_response(self.django_client, request_url, data) + response = _csrf_delete_response(self.django_client, + request_url, + json.dumps(data), + content_type="application/json") + # Response will contain remaining links from image (see test_links.py) + response = json.loads(response.content) + assert response == {"success": True} + + def test_create_share(self): + + img = self.make_image() + request_url = reverse("manage_action_containers", + args=["add", "share"]) + data = { + 'enable': 'on', + 'image': img.id.val, + 'members': self.user.id.val, + 'message': 'foobar' + } + + _post_response(self.django_client, request_url, data) + _csrf_post_response(self.django_client, request_url, data) + + def test_edit_share(self): + + # create images + images = [self.createTestImage(session=self.sf), + self.createTestImage(session=self.sf)] + + sid = self.sf.getShareService().createShare( + "foobar", rtime(None), images, [self.user], [], True) + + request_url = reverse("manage_action_containers", + args=["save", "share", sid]) + + data = { + 'enable': 'on', + 'image': [i.id.val for i in images], + 'members': self.user.id.val, + 'message': 'another foobar' + } + _post_response(self.django_client, request_url, data) + _csrf_post_response(self.django_client, request_url, data) + + # remove image from share + request_url = reverse("manage_action_containers", + args=["removefromshare", "share", sid]) + data = { + 'source': images[1].id.val, + } + _post_response(self.django_client, request_url, data) + _csrf_post_response(self.django_client, request_url, data) diff --git a/components/tools/OmeroWeb/test/integration/test_csrf.py b/components/tools/OmeroWeb/test/integration/test_csrf.py index 957d43a97b4..d1a9c4f66ac 100644 --- a/components/tools/OmeroWeb/test/integration/test_csrf.py +++ b/components/tools/OmeroWeb/test/integration/test_csrf.py @@ -24,13 +24,10 @@ import omero import omero.clients -from omero.rtypes import rstring, rtime +from omero.rtypes import rstring from weblibrary import IWebTest from weblibrary import _csrf_post_response, _post_response from weblibrary import _csrf_get_response, _get_response -from weblibrary import _csrf_delete_response, _delete_response - -import json from django.test import Client from django.core.urlresolvers import reverse @@ -104,47 +101,6 @@ def test_forgot_password(self): _post_response(self.django_client, request_url, data) _csrf_post_response(self.django_client, request_url, data) - def test_add_and_rename_container(self): - - # Add project - request_url = reverse("manage_action_containers", - args=["addnewcontainer"]) - data = { - 'folder_type': 'project', - 'name': 'foobar' - } - _post_response(self.django_client, request_url, data) - response = _csrf_post_response(self.django_client, request_url, data) - pid = json.loads(response.content).get("id") - - # Add dataset to the project - request_url = reverse("manage_action_containers", - args=["addnewcontainer", "project", pid]) - data = { - 'folder_type': 'dataset', - 'name': 'foobar' - } - _post_response(self.django_client, request_url, data) - _csrf_post_response(self.django_client, request_url, data) - - # Rename project - request_url = reverse("manage_action_containers", - args=["savename", "project", pid]) - data = { - 'name': 'anotherfoobar' - } - _post_response(self.django_client, request_url, data) - _csrf_post_response(self.django_client, request_url, data) - - # Change project description - request_url = reverse("manage_action_containers", - args=["savedescription", "project", pid]) - data = { - 'description': 'anotherfoobar' - } - _post_response(self.django_client, request_url, data) - _csrf_post_response(self.django_client, request_url, data) - def test_move_data(self): group_id = self.new_group(experimenters=[self.user]).id.val @@ -171,48 +127,6 @@ def test_add_and_remove_comment(self): # Remove comment, see remove tag, # http://localhost/webclient/action/remove/[comment|tag|file]/ID/ - def test_add_edit_and_remove_tag(self): - - # Add tag - img = self.image_with_channels() - tag = self.new_tag() - request_url = reverse('annotate_tags') - data = { - 'image': img.id.val, - 'filter_mode': 'any', - 'filter_owner_mode': 'all', - 'index': 0, - 'newtags-0-description': '', - 'newtags-0-tag': 'foobar', - 'newtags-0-tagset': '', - 'newtags-INITIAL_FORMS': 0, - 'newtags-MAX_NUM_FORMS': 1000, - 'newtags-TOTAL_FORMS': 1, - 'tags': tag.id.val - } - _post_response(self.django_client, request_url, data) - _csrf_post_response(self.django_client, request_url, data) - - # Edit tag, see save container name and description - # http://localhost/webclient/action/savename/tag/ID/ - # http://localhost/webclient/action/savedescription/tag/ID/ - - # Remove tag - request_url = reverse("manage_action_containers", - args=["remove", "tag", tag.id.val]) - data = { - 'index': 0, - 'parent': "image-%i" % img.id.val - } - _post_response(self.django_client, request_url, data) - _csrf_post_response(self.django_client, request_url, data) - - # Delete tag - request_url = reverse("manage_action_containers", - args=["delete", "tag", tag.id.val]) - _post_response(self.django_client, request_url, {}) - _csrf_post_response(self.django_client, request_url, {}) - def test_attach_file(self): # Due to EOF both posts must be test separately @@ -254,92 +168,6 @@ def test_attach_file(self): # Remove file, see remove tag, # http://localhost/webclient/action/remove/[comment|tag|file]/ID/ - def test_paste_move_remove_deletamany_image(self): - - # Add dataset - request_url = reverse("manage_action_containers", - args=["addnewcontainer"]) - data = { - 'folder_type': 'dataset', - 'name': 'foobar' - } - _post_response(self.django_client, request_url, data) - response = _csrf_post_response(self.django_client, request_url, data) - did = json.loads(response.content).get("id") - - img = self.image_with_channels() - - # Link image to Dataset - request_url = reverse("api_links") - data = { - 'dataset': {did: {'image': [img.id.val]}} - } - - _post_response(self.django_client, request_url, data) - _csrf_post_response(self.django_client, - request_url, - json.dumps(data), - content_type="application/json") - - # Unlink image from Dataset - request_url = reverse("api_links") - data = { - 'dataset': {did: {'image': [img.id.val]}} - } - _delete_response(self.django_client, request_url, data) - response = _csrf_delete_response(self.django_client, - request_url, - json.dumps(data), - content_type="application/json") - # Response will contain remaining links from image (see test_links.py) - response = json.loads(response.content) - assert response == {"success": True} - - def test_create_share(self): - - img = self.image_with_channels() - request_url = reverse("manage_action_containers", - args=["add", "share"]) - data = { - 'enable': 'on', - 'image': img.id.val, - 'members': self.user.id.val, - 'message': 'foobar' - } - - _post_response(self.django_client, request_url, data) - _csrf_post_response(self.django_client, request_url, data) - - def test_edit_share(self): - - # create images - images = [self.createTestImage(session=self.sf), - self.createTestImage(session=self.sf)] - - sid = self.sf.getShareService().createShare( - "foobar", rtime(None), images, [self.user], [], True) - - request_url = reverse("manage_action_containers", - args=["save", "share", sid]) - - data = { - 'enable': 'on', - 'image': [i.id.val for i in images], - 'members': self.user.id.val, - 'message': 'another foobar' - } - _post_response(self.django_client, request_url, data) - _csrf_post_response(self.django_client, request_url, data) - - # remove image from share - request_url = reverse("manage_action_containers", - args=["removefromshare", "share", sid]) - data = { - 'source': images[1].id.val, - } - _post_response(self.django_client, request_url, data) - _csrf_post_response(self.django_client, request_url, data) - def test_edit_channel_names(self): """ diff --git a/components/tools/OmeroWeb/test/integration/test_links.py b/components/tools/OmeroWeb/test/integration/test_links.py index a4fc03c491e..587b9f548a3 100644 --- a/components/tools/OmeroWeb/test/integration/test_links.py +++ b/components/tools/OmeroWeb/test/integration/test_links.py @@ -150,6 +150,48 @@ def test_link_datasets_images(self, datasets, images): assert rsp['images'][0]['id'] == iids[0] assert rsp['images'][1]['id'] == iids[1] + def test_link_unlink_tagset_tags(self): + """ + Tests linking of tagset to tag, then unlinking + """ + tag = self.make_tag() + tagset = self.make_tag(ns=omero.constants.metadata.NSINSIGHTTAGSET) + tagId = tag.id.val + tagsetId = tagset.id.val + + links_url = reverse("api_links") + # Link tagset to tag + data = { + 'tagset': {tagsetId: {'tag': [tagId]}} + } + rsp = _csrf_post_response_json(self.django_client, links_url, data) + assert rsp == {"success": True} + + # Check that tag is listed under tagset... + tags_url = reverse("api_tags_and_tagged") + r = _get_response_json(self.django_client, tags_url, {'id': tagsetId}) + assert len(r['tags']) == 1 + assert r['tags'][0]['id'] == tagId + + # Unlink first Tag from Tagset + # data {} is same as for creating link above + response = _csrf_delete_response(self.django_client, + links_url, + json.dumps(data), + content_type="application/json") + response = json.loads(response.content) + assert response["success"] + + # Since the Delete is ansync - need to check repeatedly for deletion + for i in range(10): + rsp = _get_response_json(self.django_client, + tags_url, {'id': tagsetId}) + if len(rsp['tags']) == 0: + break + sleep(0.5) + # Check that link has been deleted + assert len(rsp['tags']) == 0 + def test_unlink_screen_plate(self, screens, plates): # Link both plates to both screens request_url = reverse("api_links") diff --git a/components/tools/OmeroWeb/test/integration/test_tags.py b/components/tools/OmeroWeb/test/integration/test_tags.py new file mode 100644 index 00000000000..8b5e770e899 --- /dev/null +++ b/components/tools/OmeroWeb/test/integration/test_tags.py @@ -0,0 +1,170 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# Copyright (C) 2016 University of Dundee & Open Microscopy Environment. +# All rights reserved. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +""" +Tests creation, linking, editing and deletion of Tags +""" + +import omero +import omero.clients +from omero.rtypes import rstring +from weblibrary import IWebTest +from weblibrary import _csrf_post_response, _post_response +from weblibrary import _get_response + +import pytest +import json +from django.core.urlresolvers import reverse + + +class TestCsrf(IWebTest): + """ + Tests creation, linking, editing and deletion of Tags + """ + + def new_tag(self): + """ + Returns a new Tag objects + """ + tag = omero.model.TagAnnotationI() + tag.textValue = rstring(self.uuid()) + tag.ns = rstring("pytest") + return self.sf.getUpdateService().saveAndReturnObject(tag) + + def test_create_tag_and_tagset(self): + """ + Creates a Tagset then a Tag within the Tagset + """ + + tsValue = 'testTagset' + request_url = reverse("manage_action_containers", + args=["addnewcontainer"]) + data = { + 'folder_type': 'tagset', + 'name': tsValue + } + response = _csrf_post_response(self.django_client, request_url, data) + tagsetId = json.loads(response.content).get("id") + + # check creation + tagset = self.query.get('TagAnnotationI', tagsetId) + assert tagset is not None + assert tagset.ns.val == omero.constants.metadata.NSINSIGHTTAGSET + assert tagset.textValue.val == tsValue + + # Add tag to the tagset + request_url = reverse("manage_action_containers", + args=["addnewcontainer", "tagset", tagsetId]) + data = { + 'folder_type': 'tag', + 'name': 'tagInTagset' + } + _post_response(self.django_client, request_url, data) + response2 = _csrf_post_response(self.django_client, request_url, data) + tagId = json.loads(response2.content).get("id") + + # Check that tag is listed under tagset... + request_url = reverse("api_tags_and_tagged") + data = {'id': tagsetId} + data = _get_response_json(self.django_client, request_url, data) + assert len(data['tags']) == 1 + assert data['tags'][0]['id'] == tagId + + @pytest.mark.parametrize("dtype", ["tagset", "tag"]) + def test_edit_tag_and_tagset(self, dtype): + """ + Creates Tag/Tagset and tests editing of name and description + """ + + request_url = reverse("manage_action_containers", + args=["addnewcontainer"]) + data = { + 'folder_type': dtype, + 'name': 'beforeEdit' + } + response = _csrf_post_response(self.django_client, request_url, data) + tagId = json.loads(response.content).get("id") + + # Edit name + request_url = reverse("manage_action_containers", + args=["savename", dtype, tagId]) + data = { + 'name': 'afterEdit' + } + response = _csrf_post_response(self.django_client, request_url, data) + + # Edit description + request_url = reverse("manage_action_containers", + args=["savedescription", dtype, tagId]) + data = { + 'description': 'New description after editing' + } + response = _csrf_post_response(self.django_client, request_url, data) + + # check edited name and description + tagset = self.query.get('TagAnnotationI', tagId) + assert tagset is not None + if dtype == "tagset": + assert tagset.ns.val == omero.constants.metadata.NSINSIGHTTAGSET + assert tagset.textValue.val == 'afterEdit' + assert tagset.description.val == 'New description after editing' + + def test_add_edit_and_remove_tag(self): + + # Add tag + img = self.make_image() + tag = self.new_tag() + request_url = reverse('annotate_tags') + data = { + 'image': img.id.val, + 'filter_mode': 'any', + 'filter_owner_mode': 'all', + 'index': 0, + 'newtags-0-description': '', + 'newtags-0-tag': 'foobar', + 'newtags-0-tagset': '', + 'newtags-INITIAL_FORMS': 0, + 'newtags-MAX_NUM_FORMS': 1000, + 'newtags-TOTAL_FORMS': 1, + 'tags': tag.id.val + } + _post_response(self.django_client, request_url, data) + _csrf_post_response(self.django_client, request_url, data) + + # Remove tag + request_url = reverse("manage_action_containers", + args=["remove", "tag", tag.id.val]) + data = { + 'index': 0, + 'parent': "image-%i" % img.id.val + } + _post_response(self.django_client, request_url, data) + _csrf_post_response(self.django_client, request_url, data) + + # Delete tag + request_url = reverse("manage_action_containers", + args=["delete", "tag", tag.id.val]) + _post_response(self.django_client, request_url, {}) + _csrf_post_response(self.django_client, request_url, {}) + + +def _get_response_json(django_client, request_url, query_string): + rsp = _get_response(django_client, request_url, + query_string, status_code=200) + return json.loads(rsp.content)