File size: 14,347 Bytes
441d880
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
import re
import datetime
import textwrap

from Config import Config
from DataDictionary import *
from FastFacts import *
from PersonalityValues import *

import pandas as pd
import numpy as np

class AttributeGroup:
    """
    Represents an attribute group (type) in the user profile.
    Fields are dynamically populated based on the group's parameters.
    """
    def __init__(self, group_name, fields):
        self.group_name = group_name
        self.fields = {field: None for field in fields}

    def set_field(self, field_name, value):
        """
        Set a value for a specific field in the attribute group.
        If the field does not exist, it is added dynamically.
        """
        if field_name not in self.fields:
            print(f"Warning: Field '{field_name}' not found in '{self.group_name}'. Adding dynamically.")
            self.fields[field_name] = None  # Add the field dynamically

        self.fields[field_name] = value  # Assign the provided value
            
    def get_field(self, field_name):
        """
        Get a value for a specific field in the attribute group.
        """
        if field_name in self.fields:
            return self.fields[field_name]
        else:
            print(f"Field '{field_name}' does not exist in the '{self.group_name}' attribute group.")            

    def to_dict(self):
        """
        Convert the attribute group to a dictionary with non-null values.
        """
        return {field: value for field, value in self.fields.items() if value is not None}

    def __repr__(self):
        """
        String representation of the attribute group with non-null fields.
        """
        fields_repr = ", ".join(f"{k}={v}" for k, v in self.fields.items() if v is not None)
        return f"{self.group_name}({fields_repr})"

          
class UserProfile:
    """
    Represents a user profile, dynamically initialised with attribute groups based on the DataDictionary.
    Includes a lazily initialised FastFacts section for storing additional facts about the user.
    """
    def __init__(self, data_dictionary):
        self.data_dictionary = data_dictionary  # Store the data dictionary for dynamic group creation
        self.attribute_groups = {}  # Dictionary to hold created attribute groups
        self.ID = None  # Unique identifier for the user profile
        self.fast_facts = None  # Lazily initialised FastFacts attribute

    def set_ID(self, ID):
        """
        Set the ID for the user profile.
        """
        self.ID = ID

    def set_field(self, group_name, field_name, value):
        """
        Set a value for a field in a specific attribute group.
        If the group does not already exist, it will be created dynamically.
        """
        if group_name not in self.attribute_groups:
            # Create the AttributeGroup only when needed
            if group_name in self.data_dictionary.get_types():
                self.attribute_groups[group_name] = AttributeGroup(
                    group_name,
                    self.data_dictionary.get_parameters(type=group_name)
                )
            else:
                print(f"Attribute group '{group_name}' is not defined in the DataDictionary.")
                return
        self.attribute_groups[group_name].set_field(field_name, value)

    def get_field(self, group_name, field_name):
        """
        Get a value for a field in a specific attribute group.
        """
        if group_name not in self.attribute_groups:
            print(f"Attribute group '{group_name}' is not found.")
            return None

        return self.attribute_groups[group_name].get_field(field_name)        
        
    def set_fields_from_list(self, attribute_type, fields, field_key="field_name", value_key="value"):
        """
        Sets fields in a UserProfile from a list of field-value pairs.

        Args:
        attribute_type (str): The name of the attribute group (e.g., "Values").
        fields (list): A list of dictionaries with field names and values to set.
        field_key (str): The key in the dictionary that corresponds to the field name.
        value_key (str): The key in the dictionary that corresponds to the value.
        """
        if not isinstance(fields, list) or not all(isinstance(field, dict) for field in fields):
            print("Fields must be a list of dictionaries.")
            return

        for field in fields:
            field_name = field.get(field_key)  # Use the specified key for field names
            value = field.get(value_key)       # Use the specified key for values

            if field_name is not None and value is not None:
                self.set_field(attribute_type, field_name, value)
            else:
                print(f"Skipping invalid field: {field}")
        
        
    def get_attributes(self, attribute_type=None):
        """
        Retrieve attributes for a specific attribute type or all attributes if no type is specified.

        Args:
        attribute_type (str, optional): The name of the attribute group to retrieve.
                                        If None, retrieves all attributes.

        Returns:
        dict: A dictionary of non-null attributes for the specified type or all types.
        """
        if attribute_type:
            if attribute_type in self.attribute_groups:
                return self.attribute_groups[attribute_type].to_dict()
            else:
                print(f"Attribute type '{attribute_type}' does not exist in this user profile.")
                return {}
        else:
            # Combine all attributes if no specific type is specified
            all_attributes = {}
            for group_name, group in self.attribute_groups.items():
                all_attributes.update({f"{group_name}_{k}": v for k, v in group.to_dict().items()})
            return all_attributes

        
    def add_fast_facts(self, facts):
        """
        Lazily initialise and add a set of facts to the FastFacts attribute.

        Args:
        facts (iterable): A collection of facts to add to FastFacts.
        """
        if not isinstance(facts, (set, list)):
            print("Facts must be provided as a set or list.")
            return

        if self.fast_facts is None:
            self.fast_facts = FastFacts()

        self.fast_facts.add_facts(facts)

    def to_dict(self, data_dictionary):
        """
        Convert the entire user profile to a dictionary.
        """
        profile_dict = {'ID': self.ID}
        
        # Iterate over all types in the data dictionary
        for attribute_type in data_dictionary.get_types():
            group_attributes = self.get_attributes(attribute_type)
            for field_name in data_dictionary.get_parameters(type=attribute_type):
                full_field_name = f"{attribute_type}_{field_name}"
        
                # Access the value directly from the dictionary
                value = group_attributes.get(field_name)
        
                # Ensure value exists before updating the profile_dict
                if value is not None:
                    profile_dict[full_field_name] = value
                else:
                    print(f"Warning: {field_name} not found in type {attribute_type}")
        
        return profile_dict

    def __repr__(self):
        """
        String representation of the user profile with attribute groups.
        """
        groups_repr = ", ".join(str(group) for group in self.attribute_groups.values())
        return f"UserProfile(ID={self.ID}, {groups_repr})"
      

    @staticmethod
    def write_user_profiles_to_excel(user_profiles, filename, data_dictionary):
        """
        Writes a list of UserProfile objects to an Excel file with columns ordered by the data dictionary.

        Args:
        user_profiles (list): List of UserProfile objects.
        filename (str): Path to the Excel file.
        data_dictionary (object): Data dictionary containing column order and metadata.
        """
        if not user_profiles:
            print("No user profiles to write.")
            return

        # Convert user profiles to a list of dictionaries
#        profiles_data = [user_profile.to_dict(data_dictionary) for user_profile in user_profiles]
        profiles_data = []

        for i, user_profile in enumerate(user_profiles):
            print(f"Processing profile {i+1}: {user_profile}")
            profile_dict = user_profile.to_dict(data_dictionary)
            print(f"Dict output: {profile_dict}")
            profiles_data.append(profile_dict)
        
        # Get the column order from the data dictionary
        column_order = ['ID'] + data_dictionary.get_columns()  # Ensure this method exists and returns the column names in the desired order

        # Create a DataFrame from the profiles data
        df = pd.DataFrame(profiles_data)

        # Ensure all columns in the data dictionary are present in the DataFrame
        for column in column_order:
            if column not in df.columns:
                print(f"Column {column} is missing")
                df[column] = None  # Add missing columns with NaN/None

        # Reorder columns based on the data dictionary
        df = df[column_order]

        # Write the DataFrame to an Excel file
        df.to_excel(filename, index=False)

        print(f"User profiles successfully written to {filename}")        

      
    @staticmethod
    def read_user_profiles_from_excel(respondent_details_file, data_dictionary, pv_criteria):
        """
        Reads a list of UserProfile objects from an Excel file and converts scores into definitions.

        Args:
        respondent_details_file (str): Path to the Excel file.
        data_dictionary (DataDictionary): Instance of DataDictionary containing valid fields.
        pv_criteria (PVAssessment): Instance of PVAssessment to retrieve text descriptions of values.

        Returns:
        list: List of UserProfile objects.
        """
        user_profiles = []

        # Read the Excel file into a DataFrame
        df = pd.read_excel(respondent_details_file)

        # Iterate over the rows in the DataFrame
        for _, row in df.iterrows():
            user_profile = UserProfile(data_dictionary)

            # Set basic fields for UserProfile if they are present
            if pd.notna(row.get('ID')):
                user_profile.set_ID(row.get('ID'))

            # Iterate over all types in the data dictionary
            for attribute_type in data_dictionary.get_types():
                for field in data_dictionary.get_parameters(type=attribute_type):
                    full_field_name = f"{attribute_type}_{field}"
                    raw_value = row.get(full_field_name)

                    if pd.notna(raw_value):
                        # Special handling for Values type
                        if attribute_type.lower() == "values":
                            try:
                                score = int(raw_value)  # Convert to integer score
                                # Get description from PVAssessment
                                description = PVAssessment.get_score_definition(field, score, pv_criteria)

                                # Set score field
                                #user_profile.set_field(attribute_type, field, score)

                                # Set description field
                                description_field_name = f"{attribute_type}_{field}_Description"
                                user_profile.set_field(attribute_type, description_field_name, description)

                            except ValueError:
                                print(f"Warning: Could not convert '{raw_value}' to an integer for field '{full_field_name}'.")
                        else:
                            # Generic field setting for non-Values types
                            user_profile.set_field(attribute_type, field, raw_value)
                                
            user_profiles.append(user_profile)

        print(f"User profiles successfully read from {respondent_details_file}")
        return user_profiles
      

class UserProfileDetail:
    def __init__(self, key, original_value, qa_check, value):
        """
        Initialize a UserProfileDetail entry.
        """
        self.key = key
        self.original_value = original_value
        self.qa_check = qa_check
        self.value = value

    def __repr__(self):
        fields = {k: v for k, v in self.__dict__.items() if v and v != "Unable to map"}
        formatted_fields = [f"{k}='{v}'" for k, v in fields.items()]
        return f"{self.__class__.__name__}: " + ", ".join(formatted_fields) + ")"

    @staticmethod
    def filter_profiles(profiles, key=None, qa_check=None, value=None):
        """
        Static method to filter user profiles by key, QA check status, or value.

        Args:
        profiles (list): List of UserProfileDetail objects.
        key (str, optional): The key to filter by.
        qa_check (str, optional): The QA check status to filter by.
        value (str, optional): The value to filter by.

        Returns:
        list: A list of UserProfileDetail entries that match the criteria.
        """
        return [
            profile for profile in profiles
            if (key is None or profile.key == key) and
               (qa_check is None or profile.qa_check == qa_check) and
               (value is None or profile.value == value)
        ]

    @staticmethod
    def generate_user_profiles(file_path):
        """
        Static method to generate a list of UserProfileDetail entries from an Excel (.xlsx) file.

        Args:
        file_path (str): The path to the Excel file containing user profile entries.

        Returns:
        list: A list of UserProfileDetail objects generated from the file.
        """
        # Read the Excel file
        df = pd.read_excel(file_path)

        profiles = []
        for _, row in df.iterrows():
            profile = UserProfileDetail(
                key=row['Key'],
                original_value=row['Value'],
                qa_check=row['QA Check'],
                value=row['Revised Value']
            )
            profiles.append(profile)
        return profiles