--
-- acs-kernel/sql/community-core-create.sql
--
-- Abstractions fundamental to any online community (or information
-- system, in general), derived in large part from the ACS 3.x
-- community-core data model by Philip Greenspun (philg@mit.edu), from
-- the ACS 3.x user-groups data model by Tracy Adams (teadams@mit.edu)
-- from Chapter 3 (The Enterprise and Its World) of David Hay's
-- book _Data_Model_Patterns_, and from Chapter 2 (Accountability)
-- of Martin Fowler's book _Analysis_Patterns_.
--
-- @author Michael Yoon (michael@arsdigita.com)
-- @author Rafael Schloming (rhs@mit.edu)
-- @author Jon Salz (jsalz@mit.edu)
--
-- @creation-date 2000-05-18
--
-- @cvs-id $Id: community-core-create.sql,v 1.31 2024/09/11 06:15:48 gustafn Exp $
--

-- HIGH PRIORITY:
--
-- * What can subtypes add to the specification of supertype
--   attributes? Extra constraints like "not null"? What about
--   "storage"? Can a subtype override how a given attribute is
--   stored?
--
-- * Can we realistically revoke INSERT and UPDATE permission on the
--   tables (users, persons, etc.) and make people use the PL/SQL API?
--   One downside is that it would then be difficult or impossible to
--   do things like "update ... set ... where ..." directly, without
--   creating a PL/SQL procedure to do it.
--
-- * Figure out how to migrate from ACS 3.x users, user_groups,
--   user_group_types, etc. to ACS 4.0 objects/parties/users/organizations;
--   also need to consider general_* and site_wide_* tables
--   (Rafi and Luke)
--
-- * Take an inventory of acs-kernel tables and other objects (some of which
--   may still be in /www/doc/sql/) and create their ACS 4 analogs, including
--   mapping over all general_* and site_wide_* data models, and make
--   appropriate adjustments to code
--   (Rafi and Yon/Luke/?).
--
-- * Create magic users: system and anonymous (do we actually need these?)
--
-- * Define and implement APIs
--
-- * Figure out user classes, e.g., treat "the set of parties that
--   have relationship X to object Y" as a party in its own right
--
-- * Explain why acs_rel_types, acs_rel_rules, and acs_rels are not
--   merely replicating the functionality of a relational database.
--
-- * acs_attribute_type should impose some rules on the min_n_values
--   and max_n_values columns of acs_attributes, e.g., it doesn't
--   really make sense for a boolean attribute to have more than
--   one value
--
-- * Add support for default values to acs_attributes.
--
-- * Add support for instance-specific attributes (e.g.,
--   user_group_member_fields)
--
-- MEDIUM PRIORITY:
--
-- * Read-only attributes?
--
-- * Do we need to store metadata about enumerations and valid ranges
--   or should we query the Oracle data dictionary for info on check
--   constraints?
--
-- * Create a "user_group_type" (an object_type with "organization"
--   as its supertype (do we need this?)
--
-- * Add in ancestor permission view, assuming that we'll use a
--   magical rel_type: "acs_acl"?
--
-- * How do we get all attribute values for objects of a specific
--   type? "We probably want some convention or standard way for
--   providing a view that joins supertypes and a type. This could
--   be automatically generated through metadata, or it could simply
--   be a convention." - Rafi
--
-- LOW PRIORITY:
--
-- * Formalize Rafi's definition of an "object": "A collection of rows
--   identified by an object ID for which we maintain metadata" or
--   something like that.
--
-- * "We definitely need some standard way of extending a supertype into
--   a subtype, and 'deleting' a subtype into a supertype. This will be
--   needed when we want to transform a 'person' into a registered
--   user, and do 'nuke user' but keep around the user's contributed
--   content and associate it with the 'person' part of that user. This
--   actually works quite nicely with standard oracle inheritance since
--   you can just insert or delete a row in the subtype table and
--   mutate the object type." - Rafi
--
-- ACS 4.1:
--
-- * Figure out what to do with pretty names (message catalog)
--
-- COMPLETED:
--
-- * Create magic parties: all_users (or just use null party_id?)
--   and registered_users
--
-- * Test out relationship attributes (making "relationship" an
--   acs_object_type)
--
-- * Create magic object_types (object, party, person, user,
--   organization) including attrs and rels
--
-- * Create constraints for creation_user and modifying_user



--
-- procedure inline_0/0
--
CREATE OR REPLACE FUNCTION inline_0(

) RETURNS integer AS $$
DECLARE
  attr_id acs_attributes.attribute_id%TYPE;
BEGIN
 --
 -- Party: the supertype of person and organization
 -- 
 PERFORM acs_object_type__create_type (
   'party',
   'Party',
   'Parties',
   'acs_object',
   'parties',
   'party_id',
    'party',
   'f',
   null,
   'party__name'
   );

 attr_id := acs_attribute__create_attribute (
	'party',
	'email',
	'string',
	'Email Address',
	'Email Addresses',
	null,
	null,
	null,
	0,
	1,
	null,
	'type_specific',
	'f'
	);

 attr_id := acs_attribute__create_attribute (
	'party',
	'url',
	'string',
	'URL',
	'URLs',
	null,
	null,
	null,
	0,
	1,
	null,
	'type_specific',
	'f'
	);

 --
 -- Person: the supertype of user
 --
 attr_id := acs_object_type__create_type (
   'person',
   'Person',
   'People',
   'party',
   'persons',
   'person_id',
   'person',
   'f',
   null,
   'person__name'
   );

 attr_id := acs_attribute__create_attribute (
        'person',
        'first_names',
        'string',
        'First Names',
        'First Names',
        null,
        null,
        null,
	0,
	1,
        null,
        'type_specific',
        'f'
      );

 attr_id := acs_attribute__create_attribute (
        'person',
        'last_name',
        'string',
        'Last Name',
        'Last Names',
        null,
        null,
        null,
	0,
	1,
        null,
        'type_specific',
        'f'
      );
  --
 -- User: people who have registered in the system
 --
 attr_id := acs_object_type__create_type (
   'user',
   'User',
   'Users',
   'person',
   'users',
   'user_id',
   'acs_user',
   'f',
   null,
   null
   );

 attr_id := acs_attribute__create_attribute (
        'user',
        'username',
        'string',
        'Username',
        'Usernames',
        null,
        null,
        null,
	0,
	1,
        null,
        'type_specific',
        'f'
      );

 attr_id := acs_attribute__create_attribute (
        'user',
        'screen_name',
        'string',
        'Screen Name',
        'Screen Names',
        null,
        null,
        null,
	0,
	1,
        null,
        'type_specific',
        'f'
      );

 attr_id := acs_attribute__create_attribute (
        'person',
        'bio',
        'string',
        'Biography',
        'Biographies',
        null,
        null,
        null,
	0,
	1,
        null,
        'type_specific',
        'f'
      );

  return 0;
END;
$$ LANGUAGE plpgsql;

select inline_0 ();

drop function inline_0 ();


-- show errors

-- ******************************************************************
-- * OPERATIONAL LEVEL
-- ******************************************************************

create table parties (
	party_id	integer not null
			constraint parties_party_id_fk references
			acs_objects (object_id)
			constraint parties_party_id_pk primary key,
	email		varchar(100)
			constraint parties_email_un unique,
	url		varchar(200)
);

create index parties_email_lower_idx on parties(lower(email));

comment on table parties is '
 Party is the supertype of person and organization. It exists because
 many other types of object can have relationships to parties.
';

comment on column parties.url is '
 We store url here so that we can always make party names hyperlinks
 without joining to any other table.
';

-------------------
-- PARTY PACKAGE --
-------------------



-- added
select define_function_args('party__new','party_id;null,object_type;party,creation_date;now(),creation_user;null,creation_ip;null,email,url;null,context_id;null');

--
-- procedure party__new/8
--
CREATE OR REPLACE FUNCTION party__new(
   new__party_id integer,          -- default null
   new__object_type varchar,       -- default 'party'
   new__creation_date timestamptz, -- default now()
   new__creation_user integer,     -- default null
   new__creation_ip varchar,       -- default null
   new__email varchar,
   new__url varchar,               -- default null
   new__context_id integer         -- default null

) RETURNS integer AS $$
DECLARE
  v_party_id                  parties.party_id%TYPE;
BEGIN
  v_party_id :=
   acs_object__new(new__party_id, new__object_type, new__creation_date, 
                   new__creation_user, new__creation_ip, new__context_id,
                   't', new__email, null);

  insert into parties
   (party_id, email, url)
  values
   (v_party_id, lower(new__email), new__url);

  return v_party_id;
  
END;
$$ LANGUAGE plpgsql;



-- added
select define_function_args('party__delete','party_id');

--
-- procedure party__delete/1
--
CREATE OR REPLACE FUNCTION party__delete(
   party_id integer
) RETURNS integer AS $$
DECLARE
BEGIN
  PERFORM acs_object__delete(party_id);

  return 0; 
END;
$$ LANGUAGE plpgsql;



-- added
select define_function_args('party__name','party_id');

--
-- procedure party__name/1
--
CREATE OR REPLACE FUNCTION party__name(
   party_id integer
) RETURNS varchar AS $$
DECLARE
BEGIN
  if party_id = -1 then
   return 'The Public';
  else
   return null;
  end if;
  
END;
$$ LANGUAGE plpgsql immutable strict;



-- added
select define_function_args('party__email','party_id');

--
-- procedure party__email/1
--
CREATE OR REPLACE FUNCTION party__email(
   email__party_id integer
) RETURNS varchar AS $$
DECLARE
BEGIN

  return email from parties where party_id = email__party_id;

END;
$$ LANGUAGE plpgsql stable strict;


-- show errors

-------------
-- PERSONS --
-------------

create table persons (
	person_id	integer not null
			constraint persons_person_id_fk
			references parties (party_id)
			constraint persons_person_id_pk primary key,
	first_names	varchar(100) not null,
	last_name	varchar(100) not null,
        bio             text
);

comment on table persons is '
 Need to handle titles like Mr., Ms., Mrs., Dr., etc. and suffixes
 like M.D., Ph.D., Jr., Sr., III, IV, etc.
';

--------------------
-- PERSON PACKAGE --
--------------------

-- create or replace package person
-- as
-- 
--  function new (
--   person_id	in persons.person_id%TYPE default null,
--   object_type	in acs_objects.object_type%TYPE
-- 		   default 'person',
--   creation_date	in acs_objects.creation_date%TYPE
-- 		   default sysdate,
--   creation_user	in acs_objects.creation_user%TYPE
-- 		   default null,
--   creation_ip	in acs_objects.creation_ip%TYPE default null,
--   email		in parties.email%TYPE,
--   url		in parties.url%TYPE default null,
--   first_names	in persons.first_names%TYPE,
--   last_name	in persons.last_name%TYPE,
--   context_id	in acs_objects.context_id%TYPE default null
--  ) return persons.person_id%TYPE;
-- 
--  procedure delete (
--   person_id	in persons.person_id%TYPE
--  );
-- 
--  function name (
--   person_id	in persons.person_id%TYPE
--  ) return varchar2;
-- 
-- end person;

-- show errors

-- create or replace package body person
-- function new

-- old define_function_args('person__new','person_id,object_type;person,creation_date;now(),creation_user,creation_ip,email,url,first_names,last_name,context_id')
-- new
select define_function_args('person__new','person_id;null,object_type;person,creation_date;now(),creation_user;null,creation_ip;null,email,url;null,first_names,last_name,context_id;null');



--
-- procedure person__new/10
--
CREATE OR REPLACE FUNCTION person__new(
   new__person_id integer,         -- default null
   new__object_type varchar,       -- default 'person'
   new__creation_date timestamptz, -- default now() -- default 'now()'
   new__creation_user integer,     -- default null
   new__creation_ip varchar,       -- default null
   new__email varchar,
   new__url varchar,               -- default null
   new__first_names varchar,
   new__last_name varchar,
   new__context_id integer         -- default null

) RETURNS integer AS $$
DECLARE
  v_person_id                 persons.person_id%TYPE;
BEGIN
  v_person_id :=
   party__new(new__person_id, new__object_type,
             new__creation_date, new__creation_user, new__creation_ip,
             new__email, new__url, new__context_id);

  update acs_objects
  set title = new__first_names || ' ' || new__last_name
  where object_id = v_person_id;

  insert into persons
   (person_id, first_names, last_name)
  values
   (v_person_id, new__first_names, new__last_name);

  return v_person_id;
  
END;
$$ LANGUAGE plpgsql;


-- procedure delete


-- added
select define_function_args('person__delete','person_id');

--
-- procedure person__delete/1
--
CREATE OR REPLACE FUNCTION person__delete(
   delete__person_id integer
) RETURNS integer AS $$
DECLARE
BEGIN
  delete from persons
  where person_id = delete__person_id;

  PERFORM party__delete(delete__person_id);

  return 0; 
END;
$$ LANGUAGE plpgsql;


-- function name


-- added
select define_function_args('person__name','person_id');

--
-- procedure person__name/1
--
CREATE OR REPLACE FUNCTION person__name(
   name__person_id integer
) RETURNS varchar AS $$
DECLARE
BEGIN

  return first_names || ' ' || last_name
  from persons
  where person_id = name__person_id;

END;
$$ LANGUAGE plpgsql stable strict;


-- function first_names


-- added
select define_function_args('person__first_names','person_id');

--
-- procedure person__first_names/1
--
CREATE OR REPLACE FUNCTION person__first_names(
   first_names__person_id integer
) RETURNS varchar AS $$
DECLARE
BEGIN
  return first_names
  from persons
  where person_id = first_names__person_id;
  
END;
$$ LANGUAGE plpgsql stable strict;


-- function last_name


-- added
select define_function_args('person__last_name','person_id');

--
-- procedure person__last_name/1
--
CREATE OR REPLACE FUNCTION person__last_name(
   last_name__person_id integer
) RETURNS varchar AS $$
DECLARE
BEGIN
  return last_name
  from persons
  where person_id = last_name__person_id;

END;
$$ LANGUAGE plpgsql stable strict;


-- show errors

create table users (
        user_id                 integer not null
                                constraint users_user_id_fk
                                references persons (person_id)
                                constraint users_user_id_pk primary key,
        authority_id            integer
                                constraint users_authority_id_fk
                                references auth_authorities(authority_id),
        username                varchar(100) 
                                constraint users_username_nn 
                                not null,
        screen_name             varchar(100)
                                constraint users_screen_name_un
                                unique,
        priv_name               integer default 0 not null,
        priv_email              integer default 5 not null,
        email_verified_p        boolean default 't',
        email_bouncing_p        boolean default 'f' not null,
        last_visit              timestamptz,
        second_to_last_visit    timestamptz,
        n_sessions              integer default 1 not null,
        -- local authentication information
        password                character varying(128),
        password_hash_algorithm character varying(100)
                                DEFAULT 'salted-sha1' NOT NULL,
        salt                    char(40),
        password_question       varchar(1000),
        password_answer         varchar(1000),
        password_changed_date   timestamptz,
        -- used for the authentication cookie
        auth_token              varchar(100),
        -- table constraints
        constraint users_authority_username_un
        unique (authority_id, username)
);

create index users_username_lower_idx on users(lower(username));
create index users_screenname_lower_idx on users(lower(screen_name));

create table user_preferences (
	user_id			integer constraint user_preferences_user_id_fk
				references users (user_id)
				constraint user_preferences_user_id_pk
				primary key,
	prefer_text_only_p	boolean default 'f',
	-- an ISO 639 language code (in lowercase)
	language_preference	char(2) default 'en',
	dont_spam_me_p		boolean default 'f',
	email_type		varchar(64),
        timezone                varchar(100)
);

CREATE OR REPLACE FUNCTION inline_1 () RETURNS integer AS $$
BEGIN

  insert into acs_object_type_tables
    (object_type, table_name, id_column)
    values
    ('user', 'user_preferences', 'user_id');
  return 0;
END;
$$ LANGUAGE plpgsql;

select inline_1 ();

drop function inline_1 ();


-- show errors


alter table acs_objects add
  constraint acs_objects_creation_user_fk
  foreign key (creation_user) references users(user_id);
alter table acs_objects add
  constraint acs_objects_modifying_user_fk
  foreign key (modifying_user) references users(user_id);

comment on table users is '
 The creation_date and creation_ip columns inherited from acs_objects
 indicate when and from where the user registered. How do we apply a
 constraint ("email must not be null") to the parent type?
';

comment on column users.last_visit is '
 Set when user reappears at site
';

comment on column users.second_to_last_visit is '
 This is what most pages query against (since last_visit will only be
 a few minutes old for most pages in a session)
';

comment on column users.n_sessions is '
 How many times this user has visited
';

----------------------
-- ACS_USER PACKAGE --
----------------------

-- create or replace package acs_user
-- as
-- 
--  function new (
--   user_id	in users.user_id%TYPE default null,
--   object_type	in acs_objects.object_type%TYPE
-- 		   default 'user',
--   creation_date	in acs_objects.creation_date%TYPE
-- 		   default sysdate,
--   creation_user	in acs_objects.creation_user%TYPE
-- 		   default null,
--   creation_ip	in acs_objects.creation_ip%TYPE default null,
--   email		in parties.email%TYPE,
--   url		in parties.url%TYPE default null,
--   first_names	in persons.first_names%TYPE,
--   last_name	in persons.last_name%TYPE,
--   password	in users.password%TYPE,
--   salt		in users.salt%TYPE,
--   password_question     in users.password_question%TYPE default null,
--   password_answer	in users.password_answer%TYPE default null,
--   screen_name	in users.screen_name%TYPE default null,
--   email_verified_p in users.email_verified_p%TYPE default 't',
--   context_id	in acs_objects.context_id%TYPE default null
--  )
--  return users.user_id%TYPE;
-- 
--  procedure approve_email (
--   user_id	in users.user_id%TYPE
--  );
-- 
--  procedure unapprove_email (
--   user_id	in users.user_id%TYPE
--  );
-- 
--  procedure delete (
--   user_id	in users.user_id%TYPE
--  );
-- 
-- end acs_user;

-- show errors

-- create or replace package body acs_user

select define_function_args('acs_user__new','user_id;null,object_type;user,creation_date;now(),creation_user;null,creation_ip;null,authority_id;null,username,email,url;null,first_names,last_name,password,salt,screen_name;null,email_verified_p;t,context_id;null');

--
-- procedure acs_user__new/16
--
CREATE OR REPLACE FUNCTION acs_user__new(
   p_user_id integer,           -- default null
   p_object_type varchar,       -- default 'user'
   p_creation_date timestamptz, -- default now()
   p_creation_user integer,     -- default null
   p_creation_ip varchar,       -- default null
   p_authority_id integer,      -- defaults to local authority
   p_username varchar, 
   p_email varchar,
   p_url varchar,               -- default null
   p_first_names varchar,
   p_last_name varchar,
   p_password char,
   p_salt char,
   p_screen_name varchar,       -- default null
   p_email_verified_p boolean,  -- default 't'
   p_context_id integer         -- default null

) RETURNS integer AS $$
DECLARE
    v_user_id                  users.user_id%TYPE;
    v_authority_id             auth_authorities.authority_id%TYPE;
    v_person_exists            integer;			
BEGIN
    v_user_id := p_user_id;

    select 1 from persons into v_person_exists where person_id = v_user_id;

    if NOT FOUND then
        v_user_id := person__new(
            v_user_id, 
            p_object_type,
            p_creation_date, 
            p_creation_user, 
            p_creation_ip,
            p_email, 
            p_url, 
            p_first_names, 
            p_last_name, 
            p_context_id
        );
    else
     update acs_objects set object_type = 'user' where object_id = v_user_id;
    end if;

    -- default to local authority
    if p_authority_id is null then
        select authority_id
        into   v_authority_id
        from   auth_authorities
        where  short_name = 'local';
    else
        v_authority_id := p_authority_id;
    end if;

    insert into users
       (user_id, authority_id, username, password, salt, screen_name, email_verified_p)
    values
       (v_user_id, v_authority_id, p_username, p_password, p_salt, p_screen_name, p_email_verified_p);

    insert into user_preferences
      (user_id)
      values
      (v_user_id);

    return v_user_id;
  
END;
$$ LANGUAGE plpgsql;




--
-- procedure acs_user__new/5
--
CREATE OR REPLACE FUNCTION acs_user__new(
   email varchar,
   fname varchar,
   lname varchar,
   pword char,
   salt char
) RETURNS integer AS $$
DECLARE
BEGIN
        return acs_user__new(null,
                             'user',
                             now(),
                             null,
                             null,                
                             email,
                             null,
                             fname,
                             lname,
                             pword,
                             salt,
                             null,
                             null,
                             null,
                             't',
                             null
                             );

END;
$$ LANGUAGE plpgsql;


-- procedure approve_email


-- added
select define_function_args('acs_user__approve_email','user_id');

--
-- procedure acs_user__approve_email/1
--
CREATE OR REPLACE FUNCTION acs_user__approve_email(
   approve_email__user_id integer
) RETURNS integer AS $$
DECLARE
BEGIN
    update users
    set email_verified_p = 't'
    where user_id = approve_email__user_id;

    return 0; 
END;
$$ LANGUAGE plpgsql;


-- procedure unapprove_email


-- added
select define_function_args('acs_user__unapprove_email','user_id');

--
-- procedure acs_user__unapprove_email/1
--
CREATE OR REPLACE FUNCTION acs_user__unapprove_email(
   unapprove_email__user_id integer
) RETURNS integer AS $$
DECLARE
BEGIN
    update users
    set email_verified_p = 'f'
    where user_id = unapprove_email__user_id;

    return 0; 
END;
$$ LANGUAGE plpgsql;


-- procedure delete


-- added
select define_function_args('acs_user__delete','user_id');

--
-- procedure acs_user__delete/1
--
CREATE OR REPLACE FUNCTION acs_user__delete(
   delete__user_id integer
) RETURNS integer AS $$
DECLARE
BEGIN
  delete from user_preferences
  where user_id = delete__user_id;

  delete from users
  where user_id = delete__user_id;

  PERFORM person__delete(delete__user_id);

  return 0; 
END;
$$ LANGUAGE plpgsql;



-- show errors