diff --git a/app/javascript/controllers/projects_controller.tsx b/app/javascript/controllers/projects_controller.tsx index a8feaaf9..b49f5d8a 100644 --- a/app/javascript/controllers/projects_controller.tsx +++ b/app/javascript/controllers/projects_controller.tsx @@ -11,6 +11,7 @@ export default class extends Controller { projectSource: Object, projectTeamId: Number, projectTeamName: String, + projectDefraHedgerowPermission: Boolean, backButtonPath: String, dbModels: Object, } @@ -19,10 +20,10 @@ export default class extends Controller { declare readonly projectSourceValue: Project declare readonly projectTeamIdValue: number declare readonly projectTeamNameValue: string + declare readonly projectDefraHedgerowPermissionValue: boolean declare readonly backButtonPathValue: string declare readonly dbModelsValue: DBModels - connect() { ReactDOM.render( , this.element ) diff --git a/app/javascript/projects/model_view.tsx b/app/javascript/projects/model_view.tsx index 9d784b47..2c743a01 100644 --- a/app/javascript/projects/model_view.tsx +++ b/app/javascript/projects/model_view.tsx @@ -13,6 +13,7 @@ import { NodeComponent } from './node_component' import { SaveModel } from './modelling/components/save_model_component' import { getDatasets } from './modelling/components/dataset_component' import { Extent } from 'ol/extent' +import { ProjectPermissions } from './project_editor' // Rete doesn't export `Transform`, so we have to re-define it ourselves export interface Transform { @@ -40,8 +41,9 @@ export interface ModelViewProps { mask: boolean maskLayer: string maskCQL: string + permissions: ProjectPermissions } -export function ModelView({ visible, initialTransform, setTransform, initialModel, setModel, createOutputLayer, deleteOutputLayer, saveMapLayer, setProcessing, autoProcessing, process, setProcess, saveModel, getDatasets, extent, zoom, mask, maskLayer, maskCQL }: ModelViewProps) { +export function ModelView({ visible, initialTransform, setTransform, initialModel, setModel, createOutputLayer, deleteOutputLayer, saveMapLayer, setProcessing, autoProcessing, process, setProcess, saveModel, getDatasets, extent, zoom, mask, maskLayer, maskCQL, permissions }: ModelViewProps) { const ref = React.useRef(null) const [editor, setEditor] = React.useState() const [engine, setEngine] = React.useState() @@ -68,7 +70,7 @@ export function ModelView({ visible, initialTransform, setTransform, initialMode }) const engine = new Engine("landscapes@1.0.0") - createDefaultComponents(saveMapLayer, saveModel, getDatasets, extent, zoom, mask, maskLayer, maskCQL).forEach(component => { + createDefaultComponents(saveMapLayer, saveModel, getDatasets, extent, zoom, mask, maskLayer, maskCQL, permissions).forEach(component => { editor.register(component) engine.register(component) }) diff --git a/app/javascript/projects/modelling/components/hedgerow_component.ts b/app/javascript/projects/modelling/components/hedgerow_component.ts new file mode 100644 index 00000000..a4e6589e --- /dev/null +++ b/app/javascript/projects/modelling/components/hedgerow_component.ts @@ -0,0 +1,92 @@ +import { NodeData, WorkerInputs, WorkerOutputs } from "rete/types/core/data" +import { BaseComponent } from "./base_component" +import { Input, Node, Output } from 'rete' +import { ProjectProperties } from "./index" +import { booleanDataSocket } from "../socket_types" +import { maskFromExtentAndShape } from "../bounding_box" +import { retrieveModelData } from "../model_retrieval" +import { createXYZ } from "ol/tilegrid" +import { TypedArray } from "d3" +import { BooleanTileGrid } from "../tile_grid" + +export class HedgerowComponent extends BaseComponent { + ProjectProperties: ProjectProperties + cachedHedgerows: BooleanTileGrid + + constructor(projectProps: ProjectProperties) { + super("Hedgerows") + this.category = "Inputs" + this.ProjectProperties = projectProps + } + + async builder(node: Node) { + node.addOutput(new Output('out', 'Hedgerows', booleanDataSocket)) + } + + async worker(node: NodeData, inputs: WorkerInputs, outputs: WorkerOutputs, ...args: unknown[]) { + + const editorNode = this.editor?.nodes.find(n => n.id === node.id) + if (editorNode === undefined) { return } + + const mask = await maskFromExtentAndShape( + this.ProjectProperties.extent, + this.ProjectProperties.zoom, + this.ProjectProperties.maskLayer, + this.ProjectProperties.maskCQL, + this.ProjectProperties.mask + ) + + if (node.outputs['out'].connections.length > 0) + { + + if(this.cachedHedgerows === undefined) { + + const tileGrid = createXYZ() + const outputTileRange = tileGrid.getTileRangeForExtentAndZ( + this.ProjectProperties.extent, + this.ProjectProperties.zoom + ) + const geotiff = await retrieveModelData( + this.ProjectProperties.extent, + 'nateng:defra_lcm_hedges', + outputTileRange + ) + + const rasters = await geotiff.readRasters({ + bbox: this.ProjectProperties.extent, + width: outputTileRange.getWidth(), + height: outputTileRange.getHeight() + }) + + const image = await geotiff.getImage() + + const result = new BooleanTileGrid( + this.ProjectProperties.zoom, + outputTileRange.minX, + outputTileRange.minY, + outputTileRange.getWidth(), + outputTileRange.getHeight() + ) + + for (let i = 0; i < (rasters[0] as TypedArray).length; i++) { + + let x = (outputTileRange.minX + i % image.getWidth()) + let y = (outputTileRange.minY + Math.floor(i / image.getWidth())) + + result.set(x, y, rasters[3][i] === 0 ? false : (mask.get(x, y) === true ? true : false)) + + } + + this.cachedHedgerows = result + outputs['out'] = result + + }else{ + + outputs['out'] = this.cachedHedgerows + + } + } + + } + +} \ No newline at end of file diff --git a/app/javascript/projects/modelling/components/index.ts b/app/javascript/projects/modelling/components/index.ts index 021dbde4..0e5e6239 100644 --- a/app/javascript/projects/modelling/components/index.ts +++ b/app/javascript/projects/modelling/components/index.ts @@ -34,6 +34,8 @@ import { ATIComponent } from "./ati_component" import { DesignationsComponent } from "./designations_component" import { ORValComponent } from "./orval_component" import { IMDComponent } from "./imd_component" +import { HedgerowComponent } from "./hedgerow_component" +import { ProjectPermissions } from "../../project_editor" export interface ProjectProperties { extent: Extent @@ -43,77 +45,85 @@ export interface ProjectProperties { maskCQL: string } -export function createDefaultComponents(saveMapLayer: SaveMapLayer, saveModel: SaveModel, getDatasets: getDatasets, extent: Extent, zoom: number, mask: boolean, maskLayer: string, maskCQL: string): BaseComponent[] { - - const projectProps = { extent, zoom, mask, maskLayer, maskCQL } - - return [ - // TODO: Replace extent, mask, zoom, maskLayer, maskCQL with projectProps in all components - - // Inputs - new UkcehLandCoverComponent(extent, zoom, mask, maskLayer, maskCQL), - new LehLandCoverComponent(extent, zoom, mask, maskLayer, maskCQL), - new IMDComponent(extent, zoom, mask, maskLayer, maskCQL), - new MlTreeHedgeComponent(extent, zoom, mask, maskLayer, maskCQL), - new BiodiversityComponent(extent, zoom, mask, maskLayer, maskCQL), - new NevoLayerComponent(extent, zoom, mask, maskLayer, maskCQL), - new ORValComponent(extent, zoom, mask, maskLayer, maskCQL), - new OSMLandUseComponent(extent, zoom, mask, maskLayer, maskCQL), - new NumericConstantComponent(), - new DigitalModelComponent(extent, zoom, mask, maskLayer, maskCQL), - new PrecompiledModelComponent(getDatasets, extent, zoom, mask, maskLayer, maskCQL), - new CensusComponent(extent, zoom, mask, maskLayer, maskCQL), - new OSGreenSpacesComponent(extent, zoom, mask, maskLayer, maskCQL), - new CROMEComponent(extent, zoom, mask, maskLayer, maskCQL), - new ATIComponent(extent, zoom, mask, maskLayer, maskCQL), - new DesignationsComponent(extent, zoom, mask, maskLayer, maskCQL), - - // Outputs - new MapLayerComponent(saveMapLayer), - new SaveModelOutputComponent(saveModel), - - // Conversions - new NumberToNumericDatasetComponent(), - new NumericDatasetToNumberComponent(), - new CategoricalComponent(), - - // Calculations - new AreaComponent(), - new DistanceMapComponent(extent, zoom, mask, maskLayer, maskCQL), - new ScaleFactorComponent(), - - // Charts - new BarChartComponent(), - - // Set operations - new VariadicOpComponent('Union', '⋃', booleanDataSocket, booleanDataSocket, 'Set operations'), - new VariadicOpComponent('Intersection', '⋂', booleanDataSocket, booleanDataSocket, 'Set operations'), - new BinaryOpComponent('Set difference', '−', booleanDataSocket, booleanDataSocket, 'Set operations', projectProps), - new VariadicOpComponent('Symmetric difference', 'Δ', booleanDataSocket, booleanDataSocket, 'Set operations'), - new UnaryOpComponent('Complement', '′', 'postfix', booleanDataSocket, booleanDataSocket, 'Set operations', projectProps), - - // Arithmetic - new MaskNumericDataComponent(), - new ExpressionComponent(), - new BinaryOpComponent('Min', '', numericNumberDataSocket, numericNumberDataSocket, 'Arithmetic', projectProps), - new BinaryOpComponent('Max', '', numericNumberDataSocket, numericNumberDataSocket, 'Arithmetic', projectProps), - new VariadicOpComponent('Sum', '∑', numericDataSocket, numericDataSocket, 'Arithmetic', 'Sum all inputs'), - new VariadicOpComponent('Merge', '', numericDataSocket, numericDataSocket, 'Arithmetic', 'Merge all inputs into a single dataset, NaN logic is overridden'), - new VariadicOpComponent('Product', '∏', numericDataSocket, numericDataSocket, 'Arithmetic'), - new BinaryOpComponent('Add', '+', numericNumberDataSocket, numericNumberDataSocket, 'Arithmetic', projectProps), - new BinaryOpComponent('Subtract', '−', numericNumberDataSocket, numericNumberDataSocket, 'Arithmetic', projectProps), - new BinaryOpComponent('Multiply', '×', numericNumberDataSocket, numericNumberDataSocket, 'Arithmetic', projectProps), - new BinaryOpComponent('Divide', '÷', numericNumberDataSocket, numericNumberDataSocket, 'Arithmetic', projectProps), - new BinaryOpComponent('Power', '^', numericNumberDataSocket, numericNumberDataSocket, 'Arithmetic', projectProps), - new UnaryOpComponent('Negate', '−', 'prefix', numericDataSocket, numericDataSocket, 'Arithmetic', projectProps), - new UnaryOpComponent('Reciprocal', '⁻¹', 'postfix', numericDataSocket, numericDataSocket, 'Arithmetic', projectProps), - new BinaryOpComponent('Less', '<', numericNumberDataSocket, booleanDataSocket, 'Arithmetic', projectProps), - new BinaryOpComponent('Greater', '>', numericNumberDataSocket, booleanDataSocket, 'Arithmetic', projectProps), - new ReplaceNaNComponent(), - - // DEBUG TOOLS - new CellAreaComponent(), - new RescaleComponent(), - - ] +export function createDefaultComponents(saveMapLayer: SaveMapLayer, saveModel: SaveModel, getDatasets: getDatasets, extent: Extent, zoom: number, mask: boolean, maskLayer: string, maskCQL: string, permissions: ProjectPermissions): BaseComponent[] { + + const projectProps: ProjectProperties = { extent, zoom, mask, maskLayer, maskCQL } + + const restrictedComponents: BaseComponent[] = [] + + // Team permissions restrict some components. Add them here. + if (permissions.DefraHedgerows) restrictedComponents.push(new HedgerowComponent(projectProps)) + + // Freely available components here. + const components : BaseComponent[] = [ + // TODO: Replace extent, mask, zoom, maskLayer, maskCQL with projectProps in all components + + // Inputs + new UkcehLandCoverComponent(extent, zoom, mask, maskLayer, maskCQL), + new LehLandCoverComponent(extent, zoom, mask, maskLayer, maskCQL), + new IMDComponent(extent, zoom, mask, maskLayer, maskCQL), + new MlTreeHedgeComponent(extent, zoom, mask, maskLayer, maskCQL), + new BiodiversityComponent(extent, zoom, mask, maskLayer, maskCQL), + new NevoLayerComponent(extent, zoom, mask, maskLayer, maskCQL), + new ORValComponent(extent, zoom, mask, maskLayer, maskCQL), + new OSMLandUseComponent(extent, zoom, mask, maskLayer, maskCQL), + new NumericConstantComponent(), + new DigitalModelComponent(extent, zoom, mask, maskLayer, maskCQL), + new PrecompiledModelComponent(getDatasets, extent, zoom, mask, maskLayer, maskCQL), + new CensusComponent(extent, zoom, mask, maskLayer, maskCQL), + new OSGreenSpacesComponent(extent, zoom, mask, maskLayer, maskCQL), + new CROMEComponent(extent, zoom, mask, maskLayer, maskCQL), + new ATIComponent(extent, zoom, mask, maskLayer, maskCQL), + new DesignationsComponent(extent, zoom, mask, maskLayer, maskCQL), + + // Outputs + new MapLayerComponent(saveMapLayer), + new SaveModelOutputComponent(saveModel), + + // Conversions + new NumberToNumericDatasetComponent(), + new NumericDatasetToNumberComponent(), + new CategoricalComponent(), + + // Calculations + new AreaComponent(), + new DistanceMapComponent(extent, zoom, mask, maskLayer, maskCQL), + new ScaleFactorComponent(), + + // Charts + new BarChartComponent(), + + // Set operations + new VariadicOpComponent('Union', '⋃', booleanDataSocket, booleanDataSocket, 'Set operations'), + new VariadicOpComponent('Intersection', '⋂', booleanDataSocket, booleanDataSocket, 'Set operations'), + new BinaryOpComponent('Set difference', '−', booleanDataSocket, booleanDataSocket, 'Set operations', projectProps), + new VariadicOpComponent('Symmetric difference', 'Δ', booleanDataSocket, booleanDataSocket, 'Set operations'), + new UnaryOpComponent('Complement', '′', 'postfix', booleanDataSocket, booleanDataSocket, 'Set operations', projectProps), + + // Arithmetic + new MaskNumericDataComponent(), + new ExpressionComponent(), + new BinaryOpComponent('Min', '', numericNumberDataSocket, numericNumberDataSocket, 'Arithmetic', projectProps), + new BinaryOpComponent('Max', '', numericNumberDataSocket, numericNumberDataSocket, 'Arithmetic', projectProps), + new VariadicOpComponent('Sum', '∑', numericDataSocket, numericDataSocket, 'Arithmetic', 'Sum all inputs'), + new VariadicOpComponent('Merge', '', numericDataSocket, numericDataSocket, 'Arithmetic', 'Merge all inputs into a single dataset, NaN logic is overridden'), + new VariadicOpComponent('Product', '∏', numericDataSocket, numericDataSocket, 'Arithmetic'), + new BinaryOpComponent('Add', '+', numericNumberDataSocket, numericNumberDataSocket, 'Arithmetic', projectProps), + new BinaryOpComponent('Subtract', '−', numericNumberDataSocket, numericNumberDataSocket, 'Arithmetic', projectProps), + new BinaryOpComponent('Multiply', '×', numericNumberDataSocket, numericNumberDataSocket, 'Arithmetic', projectProps), + new BinaryOpComponent('Divide', '÷', numericNumberDataSocket, numericNumberDataSocket, 'Arithmetic', projectProps), + new BinaryOpComponent('Power', '^', numericNumberDataSocket, numericNumberDataSocket, 'Arithmetic', projectProps), + new UnaryOpComponent('Negate', '−', 'prefix', numericDataSocket, numericDataSocket, 'Arithmetic', projectProps), + new UnaryOpComponent('Reciprocal', '⁻¹', 'postfix', numericDataSocket, numericDataSocket, 'Arithmetic', projectProps), + new BinaryOpComponent('Less', '<', numericNumberDataSocket, booleanDataSocket, 'Arithmetic', projectProps), + new BinaryOpComponent('Greater', '>', numericNumberDataSocket, booleanDataSocket, 'Arithmetic', projectProps), + new ReplaceNaNComponent(), + + // DEBUG TOOLS + new CellAreaComponent(), + new RescaleComponent(), + + ] + + return components.concat(restrictedComponents) } diff --git a/app/javascript/projects/project_editor.tsx b/app/javascript/projects/project_editor.tsx index 24cbc98d..ac3bdf58 100644 --- a/app/javascript/projects/project_editor.tsx +++ b/app/javascript/projects/project_editor.tsx @@ -21,6 +21,10 @@ export enum Tab { ModelView, } +export interface ProjectPermissions { + DefraHedgerows: boolean +} + interface ProjectEditorProps { projectId: number projectSource: Project @@ -28,8 +32,9 @@ interface ProjectEditorProps { dbModels: DBModels teamId: number teamName: string + permissions: ProjectPermissions } -export function ProjectEditor({ projectId, projectSource, backButtonPath, dbModels, teamId, teamName }: ProjectEditorProps) { +export function ProjectEditor({ projectId, projectSource, backButtonPath, dbModels, teamId, teamName, permissions }: ProjectEditorProps) { const [state, dispatch] = React.useReducer(reduce, { project: { ...defaultProject, ...projectSource }, hasUnsavedChanges: false, @@ -249,6 +254,7 @@ export function ProjectEditor({ projectId, projectSource, backButtonPath, dbMode mask={projectMask} maskLayer={projectMaskSource} maskCQL={projectMaskCQL} + permissions={permissions} /> diff --git a/app/models/permission.rb b/app/models/permission.rb new file mode 100644 index 00000000..400170ec --- /dev/null +++ b/app/models/permission.rb @@ -0,0 +1,14 @@ +class Permission < ApplicationRecord + has_many :team_permissions, dependent: :destroy + has_many :teams, through: :team_permissions + + after_create :assign_to_all_teams + + private + + def assign_to_all_teams + Team.all.each do |team| + TeamPermission.create(team: team, permission: self, enabled: false) + end + end +end diff --git a/app/models/project.rb b/app/models/project.rb index 2007ceae..bf0261e3 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -46,4 +46,13 @@ def parse_extent def duplicate dup end + + def defra_hedgerow_permission + p = Permission.find_by(name: 'defra_hedgerow') + return false unless p + + tp = team.team_permissions.find_by(permission: p) + tp ? tp.enabled : false + end + end diff --git a/app/models/team.rb b/app/models/team.rb index b7bfa4bd..e43ddd53 100644 --- a/app/models/team.rb +++ b/app/models/team.rb @@ -11,5 +11,19 @@ class Team < ApplicationRecord has_many :map_tile_layers, through: :regions has_many :overlays, through: :regions + has_many :team_permissions + has_many :permissions, through: :team_permissions + + after_create :assign_permissions + validates :name, presence: true + + private + + def assign_permissions + Permission.all.each do |permission| + TeamPermission.create(team: self, permission: permission, enabled: false) + end + end + end diff --git a/app/models/team_permission.rb b/app/models/team_permission.rb new file mode 100644 index 00000000..7ad7144d --- /dev/null +++ b/app/models/team_permission.rb @@ -0,0 +1,4 @@ +class TeamPermission < ApplicationRecord + belongs_to :team + belongs_to :permission +end diff --git a/app/views/projects/show.html.erb b/app/views/projects/show.html.erb index 83bc44a0..a39658cd 100644 --- a/app/views/projects/show.html.erb +++ b/app/views/projects/show.html.erb @@ -4,6 +4,7 @@ data-projects-project-team-id-value="<%= @project.team.id %>" data-projects-project-team-name-value="<%= @project.team.name %>" data-projects-project-source-value="<%= @project.source.to_json %>" + data-projects-project-defra-hedgerow-permission-value="<%= @project.defra_hedgerow_permission %>" data-projects-back-button-path-value="<%= team_projects_path(@project.team) %>" data-projects-db-models-value="<%= render partial: "defs", formats: [:json] %>" > diff --git a/db/migrate/20240611095407_create_permissions.rb b/db/migrate/20240611095407_create_permissions.rb new file mode 100644 index 00000000..33c6f078 --- /dev/null +++ b/db/migrate/20240611095407_create_permissions.rb @@ -0,0 +1,9 @@ +class CreatePermissions < ActiveRecord::Migration[6.1] + def change + create_table :permissions do |t| + t.string :name + + t.timestamps + end + end +end diff --git a/db/migrate/20240611095437_create_team_permissions.rb b/db/migrate/20240611095437_create_team_permissions.rb new file mode 100644 index 00000000..427ab8a3 --- /dev/null +++ b/db/migrate/20240611095437_create_team_permissions.rb @@ -0,0 +1,11 @@ +class CreateTeamPermissions < ActiveRecord::Migration[6.1] + def change + create_table :team_permissions do |t| + t.references :team, null: false, foreign_key: true + t.references :permission, null: false, foreign_key: true + t.boolean :enabled + + t.timestamps + end + end +end diff --git a/db/migrate/20240611101037_add_cascade_delete_to_team_permissions.rb b/db/migrate/20240611101037_add_cascade_delete_to_team_permissions.rb new file mode 100644 index 00000000..60595bde --- /dev/null +++ b/db/migrate/20240611101037_add_cascade_delete_to_team_permissions.rb @@ -0,0 +1,9 @@ +class AddCascadeDeleteToTeamPermissions < ActiveRecord::Migration[6.1] + def change + remove_foreign_key :team_permissions, :permissions + add_foreign_key :team_permissions, :permissions, on_delete: :cascade + + remove_foreign_key :team_permissions, :teams + add_foreign_key :team_permissions, :teams, on_delete: :cascade + end +end diff --git a/db/migrate/20240611101639_add_hedgerow_permission.rb b/db/migrate/20240611101639_add_hedgerow_permission.rb new file mode 100644 index 00000000..45dbc3f7 --- /dev/null +++ b/db/migrate/20240611101639_add_hedgerow_permission.rb @@ -0,0 +1,21 @@ +class AddHedgerowPermission < ActiveRecord::Migration[6.1] + + def up + permission = Permission.find_or_create_by(name: 'defra_hedgerow') + + Team.all.each do |team| + TeamPermission.find_or_create_by(team: team, permission: permission, enabled: false) + end + + end + + def down + permission = Permission.find_by(name: 'defra_hedgerow') + + if permission + TeamPermission.where(permission: permission).destroy_all + permission.destroy + end + end + +end diff --git a/db/schema.rb b/db/schema.rb index 06655d77..78396691 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2024_05_21_101847) do +ActiveRecord::Schema.define(version: 2024_06_11_101639) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -216,6 +216,12 @@ t.index ["region_id"], name: "index_overlays_on_region_id" end + create_table "permissions", force: :cascade do |t| + t.string "name" + t.datetime "created_at", precision: 6, null: false + t.datetime "updated_at", precision: 6, null: false + end + create_table "projects", force: :cascade do |t| t.bigint "team_id", null: false t.jsonb "source", default: {}, null: false @@ -232,6 +238,16 @@ t.index ["team_id"], name: "index_regions_on_team_id" end + create_table "team_permissions", force: :cascade do |t| + t.bigint "team_id", null: false + t.bigint "permission_id", null: false + t.boolean "enabled" + t.datetime "created_at", precision: 6, null: false + t.datetime "updated_at", precision: 6, null: false + t.index ["permission_id"], name: "index_team_permissions_on_permission_id" + t.index ["team_id"], name: "index_team_permissions_on_team_id" + end + create_table "teams", force: :cascade do |t| t.string "name" t.datetime "created_at", precision: 6, null: false @@ -284,5 +300,7 @@ add_foreign_key "overlays", "regions" add_foreign_key "projects", "teams" add_foreign_key "regions", "teams" + add_foreign_key "team_permissions", "permissions", on_delete: :cascade + add_foreign_key "team_permissions", "teams", on_delete: :cascade add_foreign_key "training_data_downloads", "labelling_groups" end diff --git a/test/fixtures/permissions.yml b/test/fixtures/permissions.yml new file mode 100644 index 00000000..7d412240 --- /dev/null +++ b/test/fixtures/permissions.yml @@ -0,0 +1,7 @@ +# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html + +one: + name: MyString + +two: + name: MyString diff --git a/test/fixtures/team_permissions.yml b/test/fixtures/team_permissions.yml new file mode 100644 index 00000000..2084ca09 --- /dev/null +++ b/test/fixtures/team_permissions.yml @@ -0,0 +1,11 @@ +# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html + +one: + team: one + permission: one + enabled: false + +two: + team: two + permission: two + enabled: false diff --git a/test/models/permission_test.rb b/test/models/permission_test.rb new file mode 100644 index 00000000..d823d7f5 --- /dev/null +++ b/test/models/permission_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class PermissionTest < ActiveSupport::TestCase + # test "the truth" do + # assert true + # end +end diff --git a/test/models/team_permission_test.rb b/test/models/team_permission_test.rb new file mode 100644 index 00000000..85d75295 --- /dev/null +++ b/test/models/team_permission_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class TeamPermissionTest < ActiveSupport::TestCase + # test "the truth" do + # assert true + # end +end