Browse Source

WIP: Validation and documentation

Maarten van den Berg 8 years ago
parent
commit
eb9b3d156c

+ 6 - 0
.gitignore

18
 # Ignore all logfiles and tempfiles.
18
 # Ignore all logfiles and tempfiles.
19
 /log/*
19
 /log/*
20
 /tmp/*
20
 /tmp/*
21
+/doc_gen/*
21
 !/log/.keep
22
 !/log/.keep
22
 !/tmp/.keep
23
 !/tmp/.keep
24
+!/doc_gen/.keep
23
 public/assets/*
25
 public/assets/*
26
+.yardoc
24
 
27
 
25
 # Ignore Byebug command history file.
28
 # Ignore Byebug command history file.
26
 .byebug_history
29
 .byebug_history
30
+
31
+# Ignore IDEA
32
+.idea

+ 1 - 0
.yardopts

1
+--output-dir doc_gen

+ 3 - 0
Gemfile

37
 # Use Fontawesome icons
37
 # Use Fontawesome icons
38
 gem 'font-awesome-sass'
38
 gem 'font-awesome-sass'
39
 
39
 
40
+# Use YARD for documentation
41
+gem 'yard'
42
+
40
 # Use Capistrano for deployment
43
 # Use Capistrano for deployment
41
 # gem 'capistrano-rails', group: :development
44
 # gem 'capistrano-rails', group: :development
42
 
45
 

+ 2 - 0
Gemfile.lock

159
     websocket-driver (0.6.4)
159
     websocket-driver (0.6.4)
160
       websocket-extensions (>= 0.1.0)
160
       websocket-extensions (>= 0.1.0)
161
     websocket-extensions (0.1.2)
161
     websocket-extensions (0.1.2)
162
+    yard (0.9.5)
162
 
163
 
163
 PLATFORMS
164
 PLATFORMS
164
   ruby
165
   ruby
184
   tzinfo-data
185
   tzinfo-data
185
   uglifier (>= 1.3.0)
186
   uglifier (>= 1.3.0)
186
   web-console
187
   web-console
188
+  yard
187
 
189
 
188
 BUNDLED WITH
190
 BUNDLED WITH
189
    1.13.6
191
    1.13.6

+ 74 - 5
app/models/activity.rb

1
+# An Activity represents a single continuous event that the members of a group may attend.
2
+# An Activity belongs to a group, and has many participants.
1
 class Activity < ApplicationRecord
3
 class Activity < ApplicationRecord
4
+  # @!attribute public_name
5
+  #   @return [String]
6
+  #     the name that users will see if there is no {#secret_name}, or if
7
+  #     {#show_hidden} is `false`.
8
+  #
9
+  # @!attribute secret_name
10
+  #   @return [String]
11
+  #     a name that is (be default) only visible to organizers and group
12
+  #     leaders.  Can be shown or hidden to normal participants using
13
+  #     {#show_hidden}.
14
+  #
15
+  # @!attribute description
16
+  #   @return [String]
17
+  #     a short text describing the activity. This text is always visible to
18
+  #     all users.
19
+  #
20
+  # @!attribute location
21
+  #   @return [String]
22
+  #     a short text describing where the activity will take place. Always
23
+  #     visible to all participants.
24
+  #
25
+  # @!attribute show_hidden
26
+  #   @return [Boolean]
27
+  #     Whether or not the 'secret' attributes can be viewed by the people who
28
+  #     aren't organizers or group leaders. Currently, only {#secret_name} is
29
+  #     influenced by this. More attributes may be added in the future, and
30
+  #     will be controlled by this toggle as well.
31
+  #
32
+  # @!attribute start
33
+  #   @return [TimeWithZone]
34
+  #     when the activity starts.
35
+  #
36
+  # @!attribute end
37
+  #   @return [TimeWithZone]
38
+  #     when the activity ends.
39
+  #
40
+  # @!attribute deadline
41
+  #   @return [TimeWithZone]
42
+  #     when the normal participants (everyone who isn't an organizer or group
43
+  #     leader) may not change their own attendance anymore. Disabled if set to
44
+  #     nil.
45
+
2
   belongs_to :group
46
   belongs_to :group
3
 
47
 
4
   has_many :participants
48
   has_many :participants
6
 
50
 
7
   validates :public_name, presence: true
51
   validates :public_name, presence: true
8
   validates :start, presence: true
52
   validates :start, presence: true
9
-  validate  :deadline_before_start
53
+  validate  :deadline_before_start, unless: "self.deadline.blank?"
54
+  validate  :end_after_start,       unless: "self.end.blank?"
10
 
55
 
56
+  # Get all people (not participants) that are organizers. Does not include
57
+  # group leaders, although they may modify the activity as well.
11
   def organizers
58
   def organizers
12
-    self.organizers.includes(:person).where(is_organizer: true)
59
+    self.participants.includes(:person).where(is_organizer: true)
60
+  end
61
+
62
+  # Determine whether the passed Person participates in the activity.
63
+  def is_participant?(person)
64
+    Participant.exists?(
65
+      activity_id: self.id,
66
+      person_id: person.id
67
+    )
13
   end
68
   end
14
 
69
 
15
-  def is_organizer(person)
16
-    Participant.exists?(person_id: person.id, activity_id: self.id, is_organizer: true)
70
+  # Determine whether the passed Person is an organizer for the activity.
71
+  def is_organizer?(person)
72
+    Participant.exists?(
73
+      person_id: person.id,
74
+      activity_id: self.id,
75
+      is_organizer: true
76
+    )
17
   end
77
   end
18
 
78
 
19
   private
79
   private
80
+  # Assert that the deadline for participants to change the deadline, if any,
81
+  # is set before the event starts.
20
   def deadline_before_start
82
   def deadline_before_start
21
-    if self.deadline && self.deadline > self.start
83
+    if self.deadline > self.start
22
       errors.add(:deadline, 'must be before start')
84
       errors.add(:deadline, 'must be before start')
23
     end
85
     end
24
   end
86
   end
87
+
88
+  # Assert that the activity's end, if any, occurs after the event's start.
89
+  def end_after_start
90
+    if self.end < self.start
91
+      errors.add(:end, 'must be after start')
92
+    end
93
+  end
25
 end
94
 end

+ 15 - 1
app/models/group.rb

1
+# A Group contains Members, which can organize and participate in Activities.
2
+# Some of the Members may be group leaders, with the ability to see all
3
+# information and add or remove members from the group.
1
 class Group < ApplicationRecord
4
 class Group < ApplicationRecord
5
+  # @!attribute name
6
+  #   @return [String]
7
+  #     the name of the group. Must be unique across all groups.
8
+
2
   has_many :members
9
   has_many :members
3
   has_many :people, through: :members
10
   has_many :people, through: :members
4
 
11
 
5
   has_many :activities
12
   has_many :activities
6
 
13
 
14
+  validates :name,
15
+    presence: true,
16
+    uniqueness: {
17
+      case_sensitive: false
18
+    }
19
+
20
+  # @return [Array<Member>] the members in the group who are also group leaders.
7
   def leaders
21
   def leaders
8
-    self.members.where(is_leader: true)
22
+    self.members.includes(:person).where(is_leader: true)
9
   end
23
   end
10
 end
24
 end

+ 12 - 0
app/models/member.rb

1
+# A Member represents the many-to-many relation of Groups to People. At most
2
+# one member may exist for each Person-Group combination.
1
 class Member < ApplicationRecord
3
 class Member < ApplicationRecord
4
+  # @!attribute is_leader
5
+  #   @return [Boolean]
6
+  #     whether the person is a leader in the group.
7
+
2
   belongs_to :person
8
   belongs_to :person
3
   belongs_to :group
9
   belongs_to :group
10
+
11
+  validates :person_id,
12
+    uniqueness: {
13
+      scope: :group_id,
14
+      message: "is already a member of this group"
15
+    }
4
 end
16
 end

+ 22 - 0
app/models/participant.rb

1
+# A Participant represents the many-to-many relation between People and
2
+# Activities, and contains the information on whether or not the person plans
3
+# to be present at the activity.
1
 class Participant < ApplicationRecord
4
 class Participant < ApplicationRecord
5
+  # @!attribute is_organizer
6
+  #   @return [Boolean]
7
+  #     whether the person is an organizer for this event.
8
+  #
9
+  # @!attribute attending
10
+  #   @return [Boolean]
11
+  #     whether or not the person plans to attend the activity.
12
+  #
13
+  # @!attribute notes
14
+  #   @return [String]
15
+  #     a short text indicating any irregularities, such as arriving later or
16
+  #     leaving earlier.
17
+
2
   belongs_to :person
18
   belongs_to :person
3
   belongs_to :activity
19
   belongs_to :activity
20
+
21
+  validates :person_id,
22
+    uniqueness: {
23
+      scope: :activity_id,
24
+      message: "person already participates in this activity"
25
+    }
4
 end
26
 end

+ 40 - 0
app/models/person.rb

1
+# A person represents a human being. A Person may be a Member in one or more
2
+# Groups, and be a Participant in any number of events of those Groups.
3
+# A Person may access the system by creating a User, and may have at most one
4
+# User.
1
 class Person < ApplicationRecord
5
 class Person < ApplicationRecord
6
+  # @!attribute first_name
7
+  #   @return [String]
8
+  #     the person's first name. ('Vincent' in 'Vincent van Gogh'.)
9
+  #
10
+  # @!attribute infix
11
+  #   @return [String]
12
+  #     the part of a person's surname that is not taken into account when
13
+  #     sorting by surname. ('van' in 'Vincent van Gogh'.)
14
+  #
15
+  # @!attribute last_name
16
+  #   @return [String]
17
+  #     the person's surname. ('Gogh' in 'Vincent van Gogh'.)
18
+  #
19
+  # @!attribute birth_date
20
+  #   @return [Date]
21
+  #     the person's birth date.
22
+  #
23
+  # @!attribute email
24
+  #   @return [String]
25
+  #     the person's email address.
26
+  #
27
+  # @!attribute is_admin
28
+  #   @return [Boolean]
29
+  #     whether or not the person has administrative rights.
30
+
2
   has_one :user
31
   has_one :user
3
   has_many :members
32
   has_many :members
4
   has_many :participants
33
   has_many :participants
14
   before_validation :not_admin_if_nil
43
   before_validation :not_admin_if_nil
15
   before_save :update_user_email, if: :email_changed?
44
   before_save :update_user_email, if: :email_changed?
16
 
45
 
46
+  # The person's full name.
17
   def full_name
47
   def full_name
18
     if self.infix
48
     if self.infix
19
       [self.first_name, self.infix, self.last_name].join(' ')
49
       [self.first_name, self.infix, self.last_name].join(' ')
22
     end
52
     end
23
   end
53
   end
24
 
54
 
55
+  # The person's reversed name, to sort by surname.
25
   def reversed_name
56
   def reversed_name
26
     if self.infix
57
     if self.infix
27
       [self.last_name, self.infix, self.first_name].join(', ')
58
       [self.last_name, self.infix, self.first_name].join(', ')
30
     end
61
     end
31
   end
62
   end
32
 
63
 
64
+  # All activities where this person is an organizer.
65
+  def organized_activities
66
+    self.participants.includes(:activity).where(is_leader: true)
67
+  end
68
+
33
   private
69
   private
70
+  # Assert that the person's birth date, if any, lies in the past.
34
   def birth_date_cannot_be_in_future
71
   def birth_date_cannot_be_in_future
35
     if self.birth_date && self.birth_date > Date.today
72
     if self.birth_date && self.birth_date > Date.today
36
       errors.add(:birth_date, "can't be in the future.")
73
       errors.add(:birth_date, "can't be in the future.")
37
     end
74
     end
38
   end
75
   end
39
 
76
 
77
+  # Explicitly force nil to false in the admin field.
40
   def not_admin_if_nil
78
   def not_admin_if_nil
41
     self.is_admin ||= false
79
     self.is_admin ||= false
42
   end
80
   end
43
 
81
 
82
+  # Ensure the person's user email is updated at the same time as the person's
83
+  # email.
44
   def update_user_email
84
   def update_user_email
45
     if not self.user.nil?
85
     if not self.user.nil?
46
       self.user.update!(email: self.email)
86
       self.user.update!(email: self.email)

+ 9 - 0
app/models/user.rb

1
+# A User contains the login information for a single Person, and allows the
2
+# user to log in by creating Sessions.
1
 class User < ApplicationRecord
3
 class User < ApplicationRecord
4
+  # @!attribute email
5
+  #   @return [String]
6
+  #     the user's email address. Should be the same as the associated Person's
7
+  #     email address.
8
+
2
   has_secure_password
9
   has_secure_password
3
   belongs_to :person
10
   belongs_to :person
4
 
11
 
8
   before_validation :email_same_as_person
15
   before_validation :email_same_as_person
9
 
16
 
10
   private
17
   private
18
+  # Assert that the user's email address is the same as the email address of
19
+  # the associated Person.
11
   def email_same_as_person
20
   def email_same_as_person
12
     if self.person and self.email != self.person.email
21
     if self.person and self.email != self.person.email
13
       errors.add(:email, "must be the same as associated person's email")
22
       errors.add(:email, "must be the same as associated person's email")

+ 9 - 0
db/migrate/20161231185937_enforce_many_to_many_uniqueness.rb

1
+class EnforceManyToManyUniqueness < ActiveRecord::Migration[5.0]
2
+  def change
3
+    add_index :participants, [:person_id, :activity_id], unique: true
4
+    add_index :members,      [:person_id, :group_id],    unique: true
5
+
6
+    remove_index :users, [:person_id]
7
+    add_index    :users, [:person_id], unique: true
8
+  end
9
+end

+ 4 - 2
db/schema.rb

10
 #
10
 #
11
 # It's strongly recommended that you check this file into your version control system.
11
 # It's strongly recommended that you check this file into your version control system.
12
 
12
 
13
-ActiveRecord::Schema.define(version: 20161214112504) do
13
+ActiveRecord::Schema.define(version: 20161231185937) do
14
 
14
 
15
   create_table "activities", force: :cascade do |t|
15
   create_table "activities", force: :cascade do |t|
16
     t.string   "public_name"
16
     t.string   "public_name"
40
     t.datetime "created_at", null: false
40
     t.datetime "created_at", null: false
41
     t.datetime "updated_at", null: false
41
     t.datetime "updated_at", null: false
42
     t.index ["group_id"], name: "index_members_on_group_id"
42
     t.index ["group_id"], name: "index_members_on_group_id"
43
+    t.index ["person_id", "group_id"], name: "index_members_on_person_id_and_group_id", unique: true
43
     t.index ["person_id"], name: "index_members_on_person_id"
44
     t.index ["person_id"], name: "index_members_on_person_id"
44
   end
45
   end
45
 
46
 
52
     t.datetime "created_at",   null: false
53
     t.datetime "created_at",   null: false
53
     t.datetime "updated_at",   null: false
54
     t.datetime "updated_at",   null: false
54
     t.index ["activity_id"], name: "index_participants_on_activity_id"
55
     t.index ["activity_id"], name: "index_participants_on_activity_id"
56
+    t.index ["person_id", "activity_id"], name: "index_participants_on_person_id_and_activity_id", unique: true
55
     t.index ["person_id"], name: "index_participants_on_person_id"
57
     t.index ["person_id"], name: "index_participants_on_person_id"
56
   end
58
   end
57
 
59
 
87
     t.datetime "created_at",           null: false
89
     t.datetime "created_at",           null: false
88
     t.datetime "updated_at",           null: false
90
     t.datetime "updated_at",           null: false
89
     t.index ["email"], name: "index_users_on_email", unique: true
91
     t.index ["email"], name: "index_users_on_email", unique: true
90
-    t.index ["person_id"], name: "index_users_on_person_id"
92
+    t.index ["person_id"], name: "index_users_on_person_id", unique: true
91
   end
93
   end
92
 
94
 
93
 end
95
 end

+ 0 - 0
doc_gen/.keep