Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions app/api/entities/organization_entity.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
module Entities
class OrganizationEntity < Grape::Entity
expose :id
expose :name
expose :description
expose :email
expose :is_enabled
expose :created_at
expose :updated_at

expose :users, using: Entities::UserEntity, if: { includes: :users }
end
end
205 changes: 205 additions & 0 deletions app/api/organizations_api.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
class OrganizationsApi < Grape::API
helpers AuthenticationHelpers
helpers AuthorisationHelpers

before do
authenticated?
end

# View Enrolled Organizations of a User
desc 'Find all Organizations of a User'
get '/organizations/enrolled' do
user_orgs = current_user.organizations

present user_orgs, with: Entities::OrganizationEntity
end
end


#View Organization by ID
desc 'Get organization by ID'
get '/organizations/:id' do
organization = Organization.find(params[:id])
unless authorise? current_user, organization, :view_members
error!({error: 'Not Authorized to view the organization'}, 403)
end
present organization, with: Entities::OrganizationEntity, includes: [:users]
end

#View Members of an Organization
desc'Get Organization members'
get '/organizations/:id/members' do
organization = Organization.find(params[:id])
unless authorise? current_user, organization, :view_members
error!({error: 'Not Authorized to view the organization members '}, 403)
end
present organization.users, with: Entities::User
end

#View asll Organizations (Admin)
desc 'Get all organizations (admin only)'
get '/organizations' do
unless authorise? current_user, Organization, :manage_organization
error!({ error: 'Not authorized to view all organizations' }, 403)
end

organizations = Organization.all
present organizations, with: Entities::OrganizationEntity
end

#Create New Organization
desc 'Create a new organization'
params do
requires :organization, type: Hash do
requires :name, type: String, desc:'Organization Name'
optional :description, type:String, desc:'Organization Description'
optional :email, type: String, desc:'Organization Email'
optional :is_enabled, type: Boolean, desc:'Organization Status'
end
end
post '/organizations' do
unless authorise? current_user, Organization, :manage_organization
error!({error: 'Not authorized to create an organization'}, 403)
end
org_params = ActionController::Parameters.new(params).require(:organization).permit(:name, :description, :email, :is_enabled)
# Check if organization with same name already exists
if Organization.exists?(name: org_params[:name])
error!({error: 'An organization with this name already exists'}, 422)
end
organization = Organization.new(org_params)

if organization.save
present organization, with: Entities::OrganizationEntity
else
error!({error: organization.errors.full_messages.join(', ')}, 422)
end
end


# Update an organization
desc 'Update an organization'
params do
requires :organization, type: Hash do
optional :name, type: String, desc:'Organization Name'
optional :description, type:String, desc:'Organization Description'
optional :email, type: String, desc:'Organization Email'
optional :is_enabled, type: Boolean, desc:'Organization Status'
end
end
put '/organizations/:id' do
organization = Organization.find(params[:id])
unless authorise? current_user, organization, :manage_organization
error!({error: 'Not authorized to update this organization'}, 403)
end

org_params = ActionController::Parameters.new(params).require(:organization).permit(:name, :description, :email, :is_enabled)

# Check if name is being changed and if it conflicts with existing org
if org_params[:name].present? &&
org_params[:name] != organization.name &&
Organization.where.not(id: params[:id]).exists?(name: org_params[:name])
error!({error: 'An organization with this name already exists'}, 422)
end

if organization.update(org_params)
present organization, with: Entities::OrganizationEntity
else
error!({error: organization.errors.full_messages.join(', ')}, 422)
end
end


# Delete an organization
desc 'Delete an organization'
delete '/organizations/:id' do
organization = Organization.find(params[:id])
unless authorise? current_user, organization, :manage_organization
error!({error: 'Not authorized to delete this organization'}, 403)
end

# Check if organization has members
if organization.users.any?
error!({error: 'Cannot delete organization with members. Remove all members first.'}, 422)
end

if organization.destroy
{ success: true }
else
error!({error: organization.errors.full_messages.join(', ')}, 422)
end
end

#Add Memebr to Organization
desc 'Add a member to an organization'
params do
requires :user_id, type: Integer, desc: 'The ID of the user to add'
end
post '/organizations/:id/members' do
organization = Organization.find(params[:id])
unless authorise? current_user, organization, :manage_members
error!({error: 'Not Authorized to add members in this organization'}, 403)
end
user = User.find(params[:user_id])
# Check if user is already a member
if organization.users.include?(user)
error!({ error: 'User is already a member of this organization' }, 422)
end

organization.users << user
present organization, with: Entities::OrganizationEntity

end

#Organization Switching
desc 'Set current organization for user'
params do
requires :organization_id, type: Integer, desc: 'The ID of the organization to set as current'
end
post '/users/current_organization' do
organization = Organization.find(params[:organization_id])
unless current_user.organizations.include?(organization)
error!({ error: 'User is not a member of this organization' }, 403)
end

current_user.update(current_organization_id: organization.id)
present organization, with: Entities::OrganizationEntity
end

#Add members by searching their username , name or email
desc 'Search users to add to organization'
params do
requires :query, type: String, desc: 'Search query (username, name, email)'
end
get '/organizations/:id/search_users' do
organization = Organization.find(params[:id])
unless authorise? current_user, organization, :manage_members
error!({ error: 'Not authorized to search users for this organization' }, 403)
end

# Search users not already in the organization
users = User.where("username LIKE :query OR first_name LIKE :query OR last_name LIKE :query OR email LIKE :query",
query: "%#{params[:query]}%")
.where.not(id: organization.user_ids)
.limit(20)

present users, with: Entities::User
end

desc 'Remove a member from an organization'
delete '/organizations/:id/members/:user_id' do
organization = Organization.find(params[:id])
unless authorise? current_user, organization, :manage_members
error!({error: 'Not Authorized to remove members from this organization'}, 403)
end

user = User.find(params[:user_id])
user_org = UserOrganization.find_by(user_id: user.id, organization_id: organization.id)
if user_org
user_org.destroy
present organization, with: Entities::OrganizationEntity
else
error!({error: 'User is not a member of this organization'}, 404)
end
end


64 changes: 64 additions & 0 deletions app/models/organization.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
class Organization < ApplicationRecord
has_many :user_organizations, dependent: :destroy
has_many :users, through: :user_organizations

validates :name, presence: true, uniqueness: { case_sensitive: false }

scope :enabled, -> { where(is_enabled: true) }

def to_s
name
end

def self.permissions
# what can admins do with organizations?
admin_role_permissions = [
:manage_organization,
:manage_members,
:view_members,
:view_enrolled_organizations
]

# what can members do with organizations?
convenor_role_permissions = [
:view_members,
:manage_members,
:view_enrolled_organizations
]

# what can tutors do with organizations?
tutor_role_permissions = [
:view_members,
:view_enrolled_organizations
]

# what can auditors do with organizations?
auditor_role_permissions = [
:view_members,
:view_enrolled_organizations
]

# what can students do with organizations?
students_role_permissions = [
:view_enrolled_organizations
]

# Return permissions hash
{
admin: admin_role_permissions,
convenor: convenor_role_permissions,
tutor: tutor_role_permissions,
auditor: auditor_role_permissions,
student: students_role_permissions
}
end

def role_for(user)
# If user belongs to a specific organization, they get role
if users.include?(user)
user.role
else
nil
end
end
end
2 changes: 2 additions & 0 deletions app/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,8 @@ def token_for_text?(a_token)
has_many :projects, dependent: :destroy
has_many :auth_tokens, dependent: :destroy
has_one :webcal, dependent: :destroy
has_many :user_organizations, dependent: :destroy
has_many :organizations, through: :user_organizations

# Model validations/constraints
validates :first_name, presence: true
Expand Down
7 changes: 7 additions & 0 deletions app/models/user_organization.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
class UserOrganization < ApplicationRecord

belongs_to :user
belongs_to :organization
validates :user_id , uniqueness: { scope: :organization_id }

end
20 changes: 20 additions & 0 deletions db/migrate/20250417005019_create_organizations.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
class CreateOrganizations < ActiveRecord::Migration[7.1]
def change
create_table :organizations do |t|
t.string :name, null: false
t.string :description, limit: 1200
t.string :email
t.boolean :is_enabled, default: true

t.timestamps
end
add_index :organizations, :name, unique: true

create_table :user_organizations do |t|
t.references :user, null: false, foreign_key: true
t.references :organization, null: false, foreign_key: true
t.timestamps
end
add_index :user_organizations, [:user_id, :organization_id], unique: true
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class AddCurrentOrganizationIdToUsers < ActiveRecord::Migration[7.1]
def change
add_reference :users, :current_organization, foreign_key: { to_table: :organizations }
end
end
27 changes: 26 additions & 1 deletion db/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.

ActiveRecord::Schema[7.1].define(version: 2024_05_28_223908) do
ActiveRecord::Schema[7.1].define(version: 2025_04_21_035348) do
create_table "activity_types", charset: "utf8", collation: "utf8_unicode_ci", force: :cascade do |t|
t.string "name", null: false
t.string "abbreviation", null: false
Expand Down Expand Up @@ -139,6 +139,16 @@
t.index ["user_id"], name: "index_logins_on_user_id"
end

create_table "organizations", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t|
t.string "name", null: false
t.string "description", limit: 1200
t.string "email"
t.boolean "is_enabled", default: true
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["name"], name: "index_organizations_on_name", unique: true
end

create_table "overseer_assessments", charset: "utf8", collation: "utf8_unicode_ci", force: :cascade do |t|
t.bigint "task_id", null: false
t.string "submission_timestamp", null: false
Expand Down Expand Up @@ -480,6 +490,16 @@
t.index ["teaching_period_id"], name: "index_units_on_teaching_period_id"
end

create_table "user_organizations", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t|
t.bigint "user_id", null: false
t.bigint "organization_id", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["organization_id"], name: "index_user_organizations_on_organization_id"
t.index ["user_id", "organization_id"], name: "index_user_organizations_on_user_id_and_organization_id", unique: true
t.index ["user_id"], name: "index_user_organizations_on_user_id"
end

create_table "users", charset: "utf8", collation: "utf8_unicode_ci", force: :cascade do |t|
t.string "email", default: "", null: false
t.string "encrypted_password", default: "", null: false
Expand Down Expand Up @@ -509,6 +529,8 @@
t.string "tii_eula_version"
t.datetime "tii_eula_date"
t.boolean "tii_eula_version_confirmed", default: false, null: false
t.bigint "current_organization_id"
t.index ["current_organization_id"], name: "index_users_on_current_organization_id"
t.index ["login_id"], name: "index_users_on_login_id", unique: true
t.index ["role_id"], name: "index_users_on_role_id"
end
Expand All @@ -531,4 +553,7 @@
t.index ["user_id"], name: "index_webcals_on_user_id", unique: true
end

add_foreign_key "user_organizations", "organizations"
add_foreign_key "user_organizations", "users"
add_foreign_key "users", "organizations", column: "current_organization_id"
end
1 change: 1 addition & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,4 @@ services:
MYSQL_PASSWORD: pwd
volumes:
- ../data/database:/var/lib/mysql