Browse Source

WIP: Validation and documentation

Maarten van den Berg 8 years ago
parent
commit
eb9b3d156c

+ 6 - 0
.gitignore

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

+ 1 - 0
.yardopts

@@ -0,0 +1 @@
1
+--output-dir doc_gen

+ 3 - 0
Gemfile

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

+ 2 - 0
Gemfile.lock

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

+ 74 - 5
app/models/activity.rb

@@ -1,4 +1,48 @@
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 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 46
   belongs_to :group
3 47
 
4 48
   has_many :participants
@@ -6,20 +50,45 @@ class Activity < ApplicationRecord
6 50
 
7 51
   validates :public_name, presence: true
8 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 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 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 77
   end
18 78
 
19 79
   private
80
+  # Assert that the deadline for participants to change the deadline, if any,
81
+  # is set before the event starts.
20 82
   def deadline_before_start
21
-    if self.deadline && self.deadline > self.start
83
+    if self.deadline > self.start
22 84
       errors.add(:deadline, 'must be before start')
23 85
     end
24 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 94
 end

+ 15 - 1
app/models/group.rb

@@ -1,10 +1,24 @@
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 4
 class Group < ApplicationRecord
5
+  # @!attribute name
6
+  #   @return [String]
7
+  #     the name of the group. Must be unique across all groups.
8
+
2 9
   has_many :members
3 10
   has_many :people, through: :members
4 11
 
5 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 21
   def leaders
8
-    self.members.where(is_leader: true)
22
+    self.members.includes(:person).where(is_leader: true)
9 23
   end
10 24
 end

+ 12 - 0
app/models/member.rb

@@ -1,4 +1,16 @@
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 3
 class Member < ApplicationRecord
4
+  # @!attribute is_leader
5
+  #   @return [Boolean]
6
+  #     whether the person is a leader in the group.
7
+
2 8
   belongs_to :person
3 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 16
 end

+ 22 - 0
app/models/participant.rb

@@ -1,4 +1,26 @@
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 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 18
   belongs_to :person
3 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 26
 end

+ 40 - 0
app/models/person.rb

@@ -1,4 +1,33 @@
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 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 31
   has_one :user
3 32
   has_many :members
4 33
   has_many :participants
@@ -14,6 +43,7 @@ class Person < ApplicationRecord
14 43
   before_validation :not_admin_if_nil
15 44
   before_save :update_user_email, if: :email_changed?
16 45
 
46
+  # The person's full name.
17 47
   def full_name
18 48
     if self.infix
19 49
       [self.first_name, self.infix, self.last_name].join(' ')
@@ -22,6 +52,7 @@ class Person < ApplicationRecord
22 52
     end
23 53
   end
24 54
 
55
+  # The person's reversed name, to sort by surname.
25 56
   def reversed_name
26 57
     if self.infix
27 58
       [self.last_name, self.infix, self.first_name].join(', ')
@@ -30,17 +61,26 @@ class Person < ApplicationRecord
30 61
     end
31 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 69
   private
70
+  # Assert that the person's birth date, if any, lies in the past.
34 71
   def birth_date_cannot_be_in_future
35 72
     if self.birth_date && self.birth_date > Date.today
36 73
       errors.add(:birth_date, "can't be in the future.")
37 74
     end
38 75
   end
39 76
 
77
+  # Explicitly force nil to false in the admin field.
40 78
   def not_admin_if_nil
41 79
     self.is_admin ||= false
42 80
   end
43 81
 
82
+  # Ensure the person's user email is updated at the same time as the person's
83
+  # email.
44 84
   def update_user_email
45 85
     if not self.user.nil?
46 86
       self.user.update!(email: self.email)

+ 9 - 0
app/models/user.rb

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

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

@@ -0,0 +1,9 @@
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,7 +10,7 @@
10 10
 #
11 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 15
   create_table "activities", force: :cascade do |t|
16 16
     t.string   "public_name"
@@ -40,6 +40,7 @@ ActiveRecord::Schema.define(version: 20161214112504) do
40 40
     t.datetime "created_at", null: false
41 41
     t.datetime "updated_at", null: false
42 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 44
     t.index ["person_id"], name: "index_members_on_person_id"
44 45
   end
45 46
 
@@ -52,6 +53,7 @@ ActiveRecord::Schema.define(version: 20161214112504) do
52 53
     t.datetime "created_at",   null: false
53 54
     t.datetime "updated_at",   null: false
54 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 57
     t.index ["person_id"], name: "index_participants_on_person_id"
56 58
   end
57 59
 
@@ -87,7 +89,7 @@ ActiveRecord::Schema.define(version: 20161214112504) do
87 89
     t.datetime "created_at",           null: false
88 90
     t.datetime "updated_at",           null: false
89 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 93
   end
92 94
 
93 95
 end

+ 0 - 0
doc_gen/.keep