| Name | Total Lines | Lines of Code | Total Coverage | Code Coverage |
|---|---|---|---|---|
| app/models/user.rb | 299 | 199 | 86.96%
|
81.91%
|
Code reported as executed by Ruby looks like this...and this: this line is also marked as covered.Lines considered as run by rcov, but not reported by Ruby, look like this,and this: these lines were inferred by rcov (using simple heuristics).Finally, here's a line marked as not executed.
1 require 'fastercsv' |
2 require 'digest' # required for {set,reset}_api_token |
3 require 'base64' # required for {set,reset}_api_token |
4 # required for repository actions |
5 require File.join(File.dirname(__FILE__), '..', '..', 'lib', 'repo', 'repository') |
6 |
7 # We always assume the following fields exists: |
8 # => :user_name, :last_name, :first_name |
9 # If there are added columns, add the default values to default_values |
10 class User < ActiveRecord::Base |
11 before_validation :strip_name |
12 |
13 # Group relationships |
14 has_many :memberships |
15 has_many :groupings, :through => :memberships |
16 has_many :notes, :as => :noteable, :dependent => :destroy |
17 has_many :accepted_memberships, :class_name => "Membership", :conditions => {:membership_status => [StudentMembership::STATUSES[:accepted], StudentMembership::STATUSES[:inviter]]} |
18 |
19 validates_presence_of :user_name, :last_name, :first_name |
20 validates_uniqueness_of :user_name |
21 |
22 validates_format_of :type, :with => /Student|Admin|Ta/ |
23 # role constants |
24 STUDENT = 'Student' |
25 ADMIN = 'Admin' |
26 TA = 'Ta' |
27 |
28 # Authentication constants to be used as return values |
29 # see self.authenticated? and main_controller for details |
30 AUTHENTICATE_SUCCESS = 0 # valid username/password combination |
31 AUTHENTICATE_NO_SUCH_USER = 1 # user does not exist |
32 AUTHENTICATE_BAD_PASSWORD = 2 # wrong password |
33 AUTHENTICATE_ERROR = 3 # generic/unknown error |
34 AUTHENTICATE_BAD_CHAR = 4 # invalid character in username/password |
35 AUTHENTICATE_BAD_PLATFORM = 5 # external authentication works for *NIX platforms only |
36 |
37 # Verifies if user is allowed to enter MarkUs |
38 # Returns user object representing the user with the given login. |
39 def self.authorize(login) |
40 # fetch login in database to see if it is registered. |
41 find_by_user_name(login) |
42 end |
43 |
44 # Authenticates login against its password |
45 # through a script specified by config VALIDATE_FILE |
46 def self.authenticate(login, password) |
47 # Do not allow the following characters in usernames/passwords |
48 # Right now, this is \n and \0 only, since username and password |
49 # are delimited by \n and C programs use \0 to terminate strings |
50 not_allowed_regexp = Regexp.new(/[\n\0]+/) |
51 if !(not_allowed_regexp.match(login) || not_allowed_regexp.match(password)) |
52 # Open a pipe and write to stdin of the program specified by config VALIDATE_FILE. |
53 # We could read something from the programs stdout, but there is no need |
54 # for that at the moment (you would do it by e.g. pipe.readlines) |
55 |
56 # External validation is supported on *NIX only |
57 if RUBY_PLATFORM =~ /(:?mswin|mingw)/ # should match for Windows only |
58 return AUTHENTICATE_BAD_PLATFORM |
59 end |
60 |
61 # In general, the external password validation program will return the |
62 # following codes (other than 0): |
63 # 1 means no such user |
64 # 2 means bad password |
65 # 3 is used for other error exits |
66 pipe = IO.popen( MarkusConfigurator.markus_config_validate_file, "w+" ) |
67 pipe.puts("#{login}\n#{password}") # write to stdin of markus_config_validate |
68 pipe.close |
69 m_logger = MarkusLogger.instance |
70 case $?.exitstatus |
71 when 0 |
72 m_logger.log("User '#{login}' logged in.", MarkusLogger::INFO) |
73 return AUTHENTICATE_SUCCESS |
74 when 1 |
75 m_logger.log("Login failed. Reason: No such user '#{login}'.", MarkusLogger::ERROR) |
76 return AUTHENTICATE_NO_SUCH_USER |
77 when 2 |
78 m_logger.log("Wrong username/password: User '#{login}'.", MarkusLogger::ERROR) |
79 return AUTHENTICATE_BAD_PASSWORD |
80 else |
81 m_logger.log("User '#{login}' failed to log in.", MarkusLogger::ERROR) |
82 return AUTHENTICATE_ERROR |
83 end |
84 else |
85 m_logger.log("User '#{login}' failed to log in. Username/password contained " + |
86 "illegal characters", MarkusLogger::ERROR) |
87 return AUTHENTICATE_BAD_CHAR |
88 end |
89 end |
90 |
91 |
92 #TODO: make these proper associations. They work fine for now but |
93 # they'll be slow in production |
94 def active_groupings |
95 self.groupings.find(:all, :conditions => ["memberships.membership_status != :u", { :u => StudentMembership::STATUSES[:rejected]}]) |
96 end |
97 |
98 # Helper methods ----------------------------------------------------- |
99 |
100 def admin? |
101 self.class == Admin |
102 end |
103 |
104 def ta? |
105 self.class == Ta |
106 end |
107 |
108 def student? |
109 self.class == Student |
110 end |
111 |
112 # Submission helper methods ------------------------------------------------- |
113 |
114 def submission_for(aid) |
115 grouping = grouping_for(aid) |
116 if grouping.nil? |
117 return nil |
118 end |
119 return grouping.current_submission_used |
120 end |
121 |
122 # Classlist parsing -------------------------------------------------------- |
123 def self.generate_csv_list(user_list) |
124 file_out = FasterCSV.generate do |csv| |
125 user_list.each do |user| |
126 # csv format is user_name,last_name,first_name |
127 # We check for user's section |
128 # If the user has a section, we had it to the CSV |
129 if !user.student? or user.section.nil? |
130 user_array = [user.user_name,user.last_name,user.first_name] |
131 else |
132 user_array = [user.user_name,user.last_name,user.first_name, user.section.name] |
133 end |
134 csv << user_array |
135 end |
136 end |
137 return file_out |
138 end |
139 |
140 def self.upload_user_list(user_class, user_list) |
141 num_update = 0 |
142 result = {} |
143 result[:invalid_lines] = [] # store lines that were not processed |
144 # read each line of the file and update classlist |
145 User.transaction do |
146 processed_users = [] |
147 FasterCSV.parse(user_list, :skip_blanks => true, :row_sep => :auto) do |row| |
148 # don't know how to fetch line so we concat given array |
149 next if FasterCSV.generate_line(row).strip.empty? |
150 if processed_users.include?(row[0]) |
151 result[:invalid_lines] = I18n.t('csv_upload_user_duplicate', {:user_name => row[0]}) |
152 else |
153 if User.add_user(user_class, row).nil? |
154 result[:invalid_lines] << row.join(",") |
155 else |
156 num_update += 1 |
157 processed_users.push(row[0]) |
158 end |
159 end |
160 end # end prase |
161 end |
162 result[:upload_notice] = "#{num_update} user(s) added/updated." |
163 return result |
164 end |
165 |
166 def self.add_user(user_class, row) |
167 # convert each line to a hash with FIELDS as corresponding keys |
168 # and create or update a user with the hash values |
169 #return nil if values.length < UPLOAD_FIELDS.length |
170 user_attributes = {} |
171 # Loop through the resulting array as key, value pairs |
172 |
173 user_class::CSV_UPLOAD_ORDER.zip(row) do |key, val| |
174 # append them to the hash that is returned by User.get_default_ta/student_attrs |
175 # remove the section if the user has one |
176 if key == :section_name |
177 if !val.nil? |
178 # check if the section already exist |
179 section = Section.find_or_create_by_name(val) |
180 user_attributes["section_id"] = section.id |
181 end |
182 else |
183 user_attributes[key] = val |
184 end |
185 end |
186 |
187 # Is there already a Student with this User number? |
188 current_user = user_class.find_or_create_by_user_name(user_attributes[:user_name]) |
189 current_user.attributes = user_attributes |
190 |
191 if !current_user.save |
192 return nil |
193 end |
194 |
195 return current_user |
196 end |
197 |
198 # Convenience method which returns a configuration Hash for the |
199 # repository lib |
200 def self.repo_config |
201 # create config |
202 conf = Hash.new |
203 conf["IS_REPOSITORY_ADMIN"] = MarkusConfigurator.markus_config_repository_admin? |
204 conf["REPOSITORY_PERMISSION_FILE"] = MarkusConfigurator.markus_config_repository_permission_file |
205 return conf |
206 end |
207 |
208 # Set API key for user model. The key is a |
209 # SHA2 512 bit long digest, which is in turn |
210 # MD5 digested and Base64 encoded so that it doesn't |
211 # include bad HTTP characters. |
212 # |
213 # TODO: If we end up |
214 # using this heavily we should probably let this token |
215 # expire every X days/hours/weeks. When it does, a new |
216 # token should be automatically generated. |
217 def set_api_key |
218 if self.api_key.nil? |
219 key = generate_api_key |
220 md5 = Digest::MD5.new |
221 md5.update(key) |
222 # base64 encode md5 hash |
223 self.api_key = Base64.encode64(md5.to_s).strip |
224 return self.save |
225 else |
226 return true |
227 end |
228 end |
229 |
230 # Resets the api key. Usually triggered, if the |
231 # old md5 hash has gotten into the wrong hands. |
232 def reset_api_key |
233 key = generate_api_key |
234 md5 = Digest::MD5.new |
235 md5.update(key) |
236 # base64 encode md5 hash |
237 self.api_key = Base64.encode64(md5.to_s).strip |
238 return self.save |
239 end |
240 |
241 private |
242 # Create some random, hard to guess SHA2 512 bit long |
243 # digest. |
244 def generate_api_key |
245 digest = Digest::SHA2.new(bitlen=512) |
246 # generate a unique token |
247 unique_seed = ActiveSupport::SecureRandom.hex(20) |
248 return digest.update("#{unique_seed} SECRET! #{Time.now.to_f}").to_s |
249 end |
250 |
251 # strip input string |
252 def strip_name |
253 if !self.user_name.nil? |
254 self.user_name.strip! |
255 end |
256 if !self.last_name.nil? |
257 self.last_name.strip! |
258 end |
259 if !self.first_name.nil? |
260 self.first_name.strip! |
261 end |
262 end |
263 |
264 # Adds read and write permissions for each newly created Admin or Ta user |
265 def grant_repository_permissions |
266 # If we're not the repository admin, bail out |
267 return if(self.student? or !MarkusConfigurator.markus_config_repository_admin?) |
268 |
269 conf = User.repo_config |
270 repo = Repository.get_class(MarkusConfigurator.markus_config_repository_type, |
271 conf) |
272 repo_names = Group.all.collect do |group| |
273 File.join(MarkusConfigurator.markus_config_repository_storage, |
274 group.repository_name) |
275 end |
276 repo.set_bulk_permissions(repo_names, {self.user_name => Repository::Permission::READ_WRITE}) |
277 end |
278 |
279 # Revokes read and write permissions for a deleted admin user |
280 def revoke_repository_permissions |
281 return if(self.student? or !MarkusConfigurator.markus_config_repository_admin?) |
282 |
283 conf = User.repo_config |
284 repo = Repository.get_class(MarkusConfigurator.markus_config_repository_type, conf) |
285 repo_names = Group.all.collect do |group| File.join(MarkusConfigurator.markus_config_repository_storage, group.repository_name) end |
286 repo.delete_bulk_permissions(repo_names, [self.user_name]) |
287 end |
288 |
289 def maintain_repository_permissions |
290 return if(self.student? or !MarkusConfigurator.markus_config_repository_admin?) |
291 if self.user_name_changed? |
292 conf = User.repo_config |
293 repo = Repository.get_class(MarkusConfigurator.markus_config_repository_type, conf) |
294 repo_names = Group.all.collect do |group| File.join(MarkusConfigurator.markus_config_repository_storage, group.repository_name) end |
295 repo.delete_bulk_permissions(repo_names, [self.user_name_was]) |
296 repo.set_bulk_permissions(repo_names, {self.user_name => Repository::Permission::READ_WRITE}) |
297 end |
298 end |
299 end |
Generated on Sun Feb 05 00:08:07 -0500 2012 with rcov 0.9.10