~azzar1/unity/add-show-desktop-key

« back to all changes in this revision

Viewing changes to lib/common/db.py

  • Committer: wagrant
  • Date: 2008-08-19 12:49:58 UTC
  • Revision ID: svn-v3-trunk0:2b9c9e99-6f39-0410-b283-7f802c844ae2:trunk:1031
Example worksheets: Fix the element names in worksheet 1 to be actually
                    correct. Also fix the first exercise so it too
                    functions properly.

Show diffs side-by-side

added added

removed removed

Lines of Context:
51
51
    * bool: Returns as "TRUE" or "FALSE", unquoted.
52
52
    * NoneType: Returns "NULL", unquoted.
53
53
    * common.caps.Role: Returns the role as a quoted, lowercase string.
 
54
    * time.struct_time: Returns the time as a quoted string for insertion into
 
55
        a TIMESTAMP column.
54
56
    Raises a DBException if val has an unsupported type.
55
57
    """
56
58
    # "E'" is postgres's way of making "escape" strings.
74
76
        return _escape(time.strftime(TIMESTAMP_FORMAT, val))
75
77
    else:
76
78
        raise DBException("Attempt to insert an unsupported type "
77
 
            "into the database")
 
79
            "into the database (%s)" % repr(type(val)))
 
80
 
 
81
def _parse_boolean(val):
 
82
    """
 
83
    Accepts a boolean as output from the DB (either the string 't' or 'f').
 
84
    Returns a boolean value True or False.
 
85
    Also accepts other values which mean True or False in PostgreSQL.
 
86
    If none match, raises a DBException.
 
87
    """
 
88
    # On a personal note, what sort of a language allows 7 different values
 
89
    # to denote each of True and False?? (A: SQL)
 
90
    if isinstance(val, bool):
 
91
        return val
 
92
    elif val == 't':
 
93
        return True
 
94
    elif val == 'f':
 
95
        return False
 
96
    elif val == 'true' or val == 'y' or val == 'yes' or val == '1' \
 
97
        or val == 1:
 
98
        return True
 
99
    elif val == 'false' or val == 'n' or val == 'no' or val == '0' \
 
100
        or val == 0:
 
101
        return False
 
102
    else:
 
103
        raise DBException("Invalid boolean value returned from DB")
78
104
 
79
105
def _passhash(password):
80
106
    return md5.md5(password).hexdigest()
161
187
        if dry: return query
162
188
        self.db.query(query)
163
189
 
 
190
    def return_insert(self, dict, tablename, tablefields, returning,
 
191
        disallowed=frozenset([]), dry=False):
 
192
        """Inserts a new row in a table, using data from a supplied
 
193
        dictionary (which will be checked by check_dict) and returns certain 
 
194
        fields as a dict.
 
195
        dict: Dictionary mapping column names to values. The values may be
 
196
            any of the following types:
 
197
            str, int, long, float, NoneType.
 
198
        tablename: String, name of the table to insert into. Will NOT be
 
199
            escaped - must be a valid identifier.
 
200
        returning: List of fields to return, not escaped
 
201
        tablefields, disallowed: see check_dict.
 
202
        dry: Returns the SQL query as a string, and does not execute it.
 
203
        Raises a DBException if the dictionary contains invalid fields.
 
204
        """
 
205
        if not DB.check_dict(dict, tablefields, disallowed):
 
206
            extras = set(dict.keys()) - tablefields
 
207
            raise DBException("Supplied dictionary contains invalid fields. (%s)" % (repr(extras)))
 
208
        # Build two lists concurrently: field names and values, as SQL strings
 
209
        fieldnames = []
 
210
        values = []
 
211
        for k,v in dict.items():
 
212
            fieldnames.append(k)
 
213
            values.append(_escape(v))
 
214
        if len(fieldnames) == 0: return
 
215
        fieldnames = ', '.join(fieldnames)
 
216
        values = ', '.join(values)
 
217
        returns = ', '.join(returning)
 
218
        query = ("INSERT INTO %s (%s) VALUES (%s) RETURNING (%s);"
 
219
            % (tablename, fieldnames, values, returns))
 
220
        if dry: return query
 
221
        return self.db.query(query)
 
222
 
 
223
 
164
224
    def update(self, primarydict, updatedict, tablename, tablefields,
165
225
        primary_keys, disallowed_update=frozenset([]), dry=False):
166
226
        """Updates a row in a table, matching against primarydict to find the
543
603
    UNION
544
604
        (SELECT problemid, loginid, date, text FROM problem_attempt
545
605
         AS problem_attempt (problemid, loginid, date, text)
546
 
         WHERE loginid = %d AND problemid = %d)
 
606
         WHERE loginid = %d AND problemid = %d AND active)
547
607
    )
548
608
    AS _
549
609
    ORDER BY date DESC
556
616
        else:
557
617
            return None
558
618
 
 
619
    def get_problem_attempts(self, login, exercisename, allow_inactive=True,
 
620
                             dry=False):
 
621
        """Given a login name and exercise name, returns a list of dicts, one
 
622
        for each attempt made for that exercise.
 
623
        Dicts are {'date': 'formatted_time', 'complete': bool}.
 
624
        Ordered with the newest first.
 
625
        
 
626
        Note: By default, returns de-activated problem attempts (unlike
 
627
        get_problem_stored_text).
 
628
        If allow_inactive is False, will not return disabled attempts.
 
629
 
 
630
        Note: Even if dry, will still physically call get_problem_problemid,
 
631
        which may mutate the DB, and get_user_loginid, which may fail.
 
632
        """
 
633
        problemid = self.get_problem_problemid(exercisename)
 
634
        loginid = self.get_user_loginid(login)  # May raise a DBException
 
635
        andactive = '' if allow_inactive else ' AND active'
 
636
        query = """SELECT date, complete FROM problem_attempt
 
637
    WHERE loginid = %d AND problemid = %d%s
 
638
    ORDER BY date DESC;""" % (loginid, problemid, andactive)
 
639
        if dry: return query
 
640
        result = self.db.query(query).getresult()
 
641
        # Make into dicts (could use dictresult, but want to convert values)
 
642
        return [{'date': date, 'complete': _parse_boolean(complete)}
 
643
                for date, complete in result]
 
644
 
 
645
    def get_problem_attempt(self, login, exercisename, as_of,
 
646
        allow_inactive=True, dry=False):
 
647
        """Given a login name, exercise name, and struct_time, returns the
 
648
        text of the submitted attempt for this question as of that date.
 
649
        Returns None if the user had not made an attempt on this problem at
 
650
        that date.
 
651
        
 
652
        Note: By default, returns de-activated problem attempts (unlike
 
653
        get_problem_stored_text).
 
654
        If allow_inactive is False, will not return disabled attempts.
 
655
 
 
656
        Note: Even if dry, will still physically call get_problem_problemid,
 
657
        which may mutate the DB, and get_user_loginid, which may fail.
 
658
        """
 
659
        problemid = self.get_problem_problemid(exercisename)
 
660
        loginid = self.get_user_loginid(login)  # May raise a DBException
 
661
        # Very similar to query in get_problem_stored_text, but without
 
662
        # looking in problem_save, and restricting to a certain date.
 
663
        andactive = '' if allow_inactive else ' AND active'
 
664
        query = """SELECT attempt FROM problem_attempt
 
665
    WHERE loginid = %d AND problemid = %d%s AND date <= %s
 
666
    ORDER BY date DESC
 
667
    LIMIT 1;""" % (loginid, problemid, andactive, _escape(as_of))
 
668
        if dry: return query
 
669
        result = self.db.query(query)
 
670
        if result.ntuples() == 1:
 
671
            # The user has made at least 1 attempt. Return the newest.
 
672
            return result.getresult()[0][0]
 
673
        else:
 
674
            return None
 
675
 
559
676
    def get_problem_status(self, login, exercisename, dry=False):
560
677
        """Given a login name and exercise name, returns information about the
561
678
        user's performance on that problem.
564
681
            - An int, the number of attempts they have made up to and
565
682
              including the first successful attempt (or the total number of
566
683
              attempts, if not yet successful).
 
684
        Note: exercisename may be an int, in which case it will be directly
 
685
        used as the problemid.
567
686
        """
568
 
        problemid = self.get_problem_problemid(exercisename)
 
687
        if isinstance(exercisename, int):
 
688
            problemid = exercisename
 
689
        else:
 
690
            problemid = self.get_problem_problemid(exercisename)
569
691
        loginid = self.get_user_loginid(login)  # May raise a DBException
570
692
 
571
693
        # ASSUME that it is completed, get the total number of attempts up to
575
697
        # Will return an empty table if the problem has never been
576
698
        # successfully completed.
577
699
        query = """SELECT COUNT(*) FROM problem_attempt
578
 
    WHERE loginid = %d AND problemid = %d AND date <=
 
700
    WHERE loginid = %d AND problemid = %d AND active AND date <=
579
701
        (SELECT date FROM problem_attempt
580
 
            WHERE loginid = %d AND problemid = %d AND complete = TRUE
 
702
            WHERE loginid = %d AND problemid = %d AND complete AND active
581
703
            ORDER BY date ASC
582
704
            LIMIT 1);""" % (loginid, problemid, loginid, problemid)
583
705
        if dry: return query
593
715
            # completed.
594
716
            # Return the total number of attempts, and False for success.
595
717
            query = """SELECT COUNT(*) FROM problem_attempt
596
 
    WHERE loginid = %d AND problemid = %d;""" % (loginid, problemid)
 
718
    WHERE loginid = %d AND problemid = %d AND active;""" % (loginid, problemid)
597
719
            result = self.db.query(query)
598
720
            count = int(result.getresult()[0][0])
599
721
            return (False, count)
600
722
 
 
723
    # WORKSHEET/PROBLEM ASSOCIATION AND MARKS CALCULATION
 
724
 
 
725
    def get_worksheet_mtime(self, subject, worksheet, dry=False):
 
726
        """
 
727
        For a given subject/worksheet name, gets the time the worksheet was
 
728
        last updated in the DB, if any.
 
729
        This can be used to check if there is a newer version on disk.
 
730
        Returns the timestamp as a time.struct_time, or None if the worksheet
 
731
        is not found or has no stored mtime.
 
732
        """
 
733
        try:
 
734
            r = self.get_single(
 
735
                {"subject": subject, "identifier": worksheet},
 
736
                "worksheet", ["mtime"], ["subject", "identifier"],
 
737
                dry=dry)
 
738
        except DBException:
 
739
            # Assume the worksheet is not in the DB
 
740
            return None
 
741
        if dry:
 
742
            return r
 
743
        if r["mtime"] is None:
 
744
            return None
 
745
        return time.strptime(r["mtime"], TIMESTAMP_FORMAT)
 
746
 
 
747
    def create_worksheet(self, subject, worksheet, problems=None,
 
748
        assessable=None):
 
749
        """
 
750
        Inserts or updates rows in the worksheet and worksheet_problems
 
751
        tables, to create a worksheet in the database.
 
752
        This atomically performs all operations. If the worksheet is already
 
753
        in the DB, removes it and all its associated problems and rebuilds.
 
754
        Sets the timestamp to the current time.
 
755
 
 
756
        problems is a collection of pairs. The first element of the pair is
 
757
        the problem identifier ("identifier" column of the problem table). The
 
758
        second element is an optional boolean, "optional". This can be omitted
 
759
        (so it's a 1-tuple), and then it will default to False.
 
760
 
 
761
        Problems and assessable are optional, and if omitted, will not change
 
762
        the existing data. If the worksheet does not yet exist, and assessable
 
763
        is omitted, it defaults to False.
 
764
 
 
765
        Note: As with get_problem_problemid, if a problem name is not in the
 
766
        DB, it will be added to the problem table.
 
767
        """
 
768
        self.start_transaction()
 
769
        try:
 
770
            # Use the current time as the "mtime" field
 
771
            mtime = time.localtime()
 
772
            try:
 
773
                # Get the worksheetid
 
774
                r = self.get_single(
 
775
                    {"subject": subject, "identifier": worksheet},
 
776
                    "worksheet", ["worksheetid"], ["subject", "identifier"])
 
777
                worksheetid = r["worksheetid"]
 
778
 
 
779
                # Delete any problems which might exist, if problems is
 
780
                # supplied. If it isn't, keep the existing ones.
 
781
                if problems is not None:
 
782
                    query = ("DELETE FROM worksheet_problem "
 
783
                        "WHERE worksheetid = %d;" % worksheetid)
 
784
                    self.db.query(query)
 
785
                # Update the row with the new details
 
786
                if assessable is None:
 
787
                    query = ("UPDATE worksheet "
 
788
                        "SET mtime = %s WHERE worksheetid = %d;"
 
789
                        % (_escape(mtime), worksheetid))
 
790
                else:
 
791
                    query = ("UPDATE worksheet "
 
792
                        "SET assessable = %s, mtime = %s "
 
793
                        "WHERE worksheetid = %d;"
 
794
                        % (_escape(assessable), _escape(mtime), worksheetid))
 
795
                self.db.query(query)
 
796
            except DBException:
 
797
                # Assume the worksheet is not in the DB
 
798
                # If assessable is not supplied, default to False.
 
799
                if assessable is None:
 
800
                    assessable = False
 
801
                # Create the worksheet row
 
802
                query = ("INSERT INTO worksheet "
 
803
                    "(subject, identifier, assessable, mtime) "
 
804
                    "VALUES (%s, %s, %s, %s);"""
 
805
                    % (_escape(subject), _escape(worksheet),
 
806
                    _escape(assessable), _escape(mtime)))
 
807
                self.db.query(query)
 
808
                # Now get the worksheetid again - should succeed
 
809
                r = self.get_single(
 
810
                    {"subject": subject, "identifier": worksheet},
 
811
                    "worksheet", ["worksheetid"], ["subject", "identifier"])
 
812
                worksheetid = r["worksheetid"]
 
813
 
 
814
            # Now insert each problem into the worksheet_problem table
 
815
            if problems is not None:
 
816
                for problem in problems:
 
817
                    if isinstance(problem, tuple):
 
818
                        prob_identifier = problem[0]
 
819
                        try:
 
820
                            optional = problem[1]
 
821
                        except IndexError:
 
822
                            optional = False
 
823
                    else:
 
824
                        prob_identifier = problem
 
825
                        optional = False
 
826
                    problemid = self.get_problem_problemid(prob_identifier)
 
827
                    query = ("INSERT INTO worksheet_problem "
 
828
                        "(worksheetid, problemid, optional) "
 
829
                        "VALUES (%d, %d, %s);"
 
830
                        % (worksheetid, problemid, _escape(optional)))
 
831
                    self.db.query(query)
 
832
 
 
833
            self.commit()
 
834
        except:
 
835
            self.rollback()
 
836
            raise
 
837
 
 
838
    def set_worksheet_assessable(self, subject, worksheet, assessable,
 
839
        dry=False):
 
840
        """
 
841
        Sets the "assessable" field of a worksheet without updating the mtime.
 
842
 
 
843
        IMPORTANT: This will NOT update the mtime. This is designed to allow
 
844
        updates which did not come from the worksheet XML file. It would be
 
845
        bad to update the mtime without consulting the XML file because then
 
846
        it would appear the database is up to date, when it isn't.
 
847
 
 
848
        Therefore, call this method if you are getting "assessable"
 
849
        information from outside the worksheet XML file (eg. from the subject
 
850
        XML file).
 
851
 
 
852
        Unlike create_worksheet, raises a DBException if the worksheet is not
 
853
        in the database.
 
854
        """
 
855
        return self.update({"subject": subject, "identifier": worksheet},
 
856
            {"assessable": assessable}, "worksheet", ["assessable"],
 
857
            ["subject", "identifier"], dry=dry)
 
858
 
 
859
    def worksheet_is_assessable(self, subject, worksheet, dry=False):
 
860
        r = self.get_single(
 
861
            {"subject": subject, "identifier": worksheet},
 
862
            "worksheet", ["assessable"], ["subject", "identifier"], dry=dry)
 
863
        return _parse_boolean(r["assessable"])
 
864
 
 
865
    def calculate_score_worksheet(self, login, subject, worksheet):
 
866
        """
 
867
        Calculates the score for a user on a given worksheet.
 
868
        Returns a 4-tuple of ints, consisting of:
 
869
        (No. mandatory exercises completed,
 
870
         Total no. mandatory exercises,
 
871
         No. optional exercises completed,
 
872
         Total no. optional exercises)
 
873
        """
 
874
        self.start_transaction()
 
875
        try:
 
876
            mand_done = 0
 
877
            mand_total = 0
 
878
            opt_done = 0
 
879
            opt_total = 0
 
880
            # Get a list of problems and optionality for all problems in the
 
881
            # worksheet
 
882
            query = ("""SELECT problemid, optional FROM worksheet_problem
 
883
    WHERE worksheetid = (SELECT worksheetid FROM worksheet
 
884
                         WHERE subject = %s and identifier = %s);"""
 
885
                    % (_escape(subject), _escape(worksheet)))
 
886
            result = self.db.query(query)
 
887
            # Now get the student's pass/fail for each problem in this worksheet
 
888
            for problemid, optional in result.getresult():
 
889
                done, _ = self.get_problem_status(login, problemid)
 
890
                # done is a bool, whether this student has completed that
 
891
                # problem
 
892
                if _parse_boolean(optional):
 
893
                    opt_total += 1
 
894
                    if done: opt_done += 1
 
895
                else:
 
896
                    mand_total += 1
 
897
                    if done: mand_done += 1
 
898
            self.commit()
 
899
        except:
 
900
            self.rollback()
 
901
            raise
 
902
        return mand_done, mand_total, opt_done, opt_total
 
903
 
 
904
    # ENROLMENT INFORMATION
 
905
 
 
906
    def add_enrolment(self, login, subj_code, semester, year=None, dry=False):
 
907
        """
 
908
        Enrol a student in the given offering of a subject.
 
909
        Returns True on success, False on failure (which usually means either
 
910
        the student is already enrolled in the subject, the student was not
 
911
        found, or no offering existed with the given details).
 
912
        The return value can usually be ignored.
 
913
        """
 
914
        subj_code = str(subj_code)
 
915
        semester = str(semester)
 
916
        if year is None:
 
917
            year = str(time.gmtime().tm_year)
 
918
        else:
 
919
            year = str(year)
 
920
        query = """\
 
921
INSERT INTO enrolment (loginid, offeringid)
 
922
    VALUES (
 
923
        (SELECT loginid FROM login WHERE login=%s),
 
924
        (SELECT offeringid
 
925
            FROM offering, subject, semester
 
926
                WHERE subject.subjectid = offering.subject
 
927
                AND semester.semesterid = offering.semesterid
 
928
                AND subj_code=%s AND semester=%s AND year=%s)
 
929
        );""" % (_escape(login), _escape(subj_code), _escape(semester),
 
930
                 _escape(year))
 
931
        if dry:
 
932
            return query
 
933
        try:
 
934
            result = self.db.query(query)
 
935
        except pg.ProgrammingError:
 
936
            return False
 
937
        return True
 
938
 
 
939
    # SUBJECTS AND ENROLEMENT
 
940
 
 
941
    def get_subjects(self, dry=False):
 
942
        """
 
943
        Get all subjects in IVLE.
 
944
        Returns a list of dicts (all values strings), with the keys:
 
945
        subj_code, subj_name, subj_short_name, url
 
946
        """
 
947
        return self.get_all("subject",
 
948
            ("subjectid", "subj_code", "subj_name", "subj_short_name", "url"),
 
949
            dry)
 
950
 
 
951
    def get_offering_semesters(self, subjectid, dry=False):
 
952
        """
 
953
        Get the semester information for a subject as well as providing 
 
954
        information about if the subject is active and which semester it is in.
 
955
        """
 
956
        query = """\
 
957
SELECT offeringid, subj_name, year, semester, active
 
958
FROM semester, offering, subject
 
959
WHERE offering.semesterid = semester.semesterid AND
 
960
    offering.subject = subject.subjectid AND
 
961
    offering.subject = %d;"""%subjectid
 
962
        if dry:
 
963
            return query
 
964
        results = self.db.query(query).dictresult()
 
965
        # Parse boolean varibles
 
966
        for result in results:
 
967
            result['active'] = _parse_boolean(result['active'])
 
968
        return results
 
969
 
 
970
    def get_offering_members(self, offeringid, dry=False):
 
971
        """
 
972
        Gets the logins of all the people enroled in an offering
 
973
        """
 
974
        query = """\
 
975
SELECT login.login AS login, login.fullname AS fullname
 
976
FROM login, enrolment
 
977
WHERE login.loginid = enrolment.loginid AND
 
978
    enrolment.offeringid = %d;"""%offeringid
 
979
        if dry:
 
980
            return query
 
981
        return self.db.query(query).dictresult()
 
982
 
 
983
 
 
984
    def get_enrolment(self, login, dry=False):
 
985
        """
 
986
        Get all offerings (in IVLE) the student is enrolled in.
 
987
        Returns a list of dicts (all values strings), with the keys:
 
988
        offeringid, subj_code, subj_name, subj_short_name, year, semester, url
 
989
        """
 
990
        query = """\
 
991
SELECT offering.offeringid, subj_code, subj_name, subj_short_name,
 
992
       semester.year, semester.semester, subject.url
 
993
FROM login, enrolment, offering, subject, semester
 
994
WHERE enrolment.offeringid=offering.offeringid
 
995
  AND login.loginid=enrolment.loginid
 
996
  AND offering.subject=subject.subjectid
 
997
  AND semester.semesterid=offering.semesterid
 
998
  AND enrolment.active
 
999
  AND login=%s;""" % _escape(login)
 
1000
        if dry:
 
1001
            return query
 
1002
        return self.db.query(query).dictresult()
 
1003
 
 
1004
    def get_enrolment_groups(self, login, offeringid, dry=False):
 
1005
        """
 
1006
        Get all groups the user is member of in the given offering.
 
1007
        Returns a list of dicts (all values strings), with the keys:
 
1008
        name, nick
 
1009
        """
 
1010
        query = """\
 
1011
SELECT project_group.groupnm as name, project_group.nick as nick
 
1012
FROM project_set, project_group, group_member, login
 
1013
WHERE login.login=%s
 
1014
  AND project_set.offeringid=%s
 
1015
  AND group_member.loginid=login.loginid
 
1016
  AND project_group.groupid=group_member.groupid
 
1017
  AND project_group.projectsetid=project_set.projectsetid
 
1018
""" % (_escape(login), _escape(offeringid))
 
1019
        if dry:
 
1020
            return query
 
1021
        return self.db.query(query).dictresult()
 
1022
 
 
1023
    def get_subjects_status(self, login, dry=False):
 
1024
        """
 
1025
        Get all subjects in IVLE, split into lists of enrolled and unenrolled
 
1026
        subjects.
 
1027
        Returns a tuple of lists (enrolled, unenrolled) of dicts
 
1028
        (all values strings) with the keys:
 
1029
        subj_code, subj_name, subj_short_name, url
 
1030
        """
 
1031
        enrolments = self.get_enrolment(login)
 
1032
        all_subjects = self.get_subjects()
 
1033
 
 
1034
        enrolled_set = set(x['subj_code'] for x in enrolments)
 
1035
 
 
1036
        enrolled_subjects = [x for x in all_subjects
 
1037
                             if x['subj_code'] in enrolled_set]
 
1038
        unenrolled_subjects = [x for x in all_subjects
 
1039
                               if x['subj_code'] not in enrolled_set]
 
1040
        enrolled_subjects.sort(key=lambda x: x['subj_code'])
 
1041
        unenrolled_subjects.sort(key=lambda x: x['subj_code'])
 
1042
        return (enrolled_subjects, unenrolled_subjects)
 
1043
 
 
1044
 
 
1045
    # PROJECT GROUPS
 
1046
    def get_groups_by_user(self, login, offeringid=None, dry=False):
 
1047
        """
 
1048
        Get all project groups the student is in, corresponding to a
 
1049
        particular subject offering (or all offerings, if omitted).
 
1050
        Returns a list of tuples:
 
1051
        (int groupid, str groupnm, str group_nick, bool is_member).
 
1052
        (Note: If is_member is false, it means they have just been invited to
 
1053
        this group, not a member).
 
1054
        """
 
1055
        if offeringid is None:
 
1056
            and_offering = ""
 
1057
        else:
 
1058
            and_projectset_table = ", project_set"
 
1059
            and_offering = """
 
1060
AND project_group.projectsetid = project_set.projectsetid
 
1061
AND project_set.offeringid = %s""" % _escape(offeringid)
 
1062
        # Union both the groups this user is a member of, and the groups this
 
1063
        # user is invited to.
 
1064
        query = """\
 
1065
    SELECT project_group.groupid, groupnm, project_group.nick, True
 
1066
    FROM project_group, group_member, login %(and_projectset_table)s
 
1067
    WHERE project_group.groupid = group_member.groupid
 
1068
      AND group_member.loginid = login.loginid
 
1069
      AND login = %(login)s
 
1070
      %(and_offering)s
 
1071
UNION
 
1072
    SELECT project_group.groupid, groupnm, project_group.nick, False
 
1073
    FROM project_group, group_invitation, login %(and_projectset_table)s
 
1074
    WHERE project_group.groupid = group_invitation.groupid
 
1075
      AND group_invitation.loginid = login.loginid
 
1076
      AND login = %(login)s
 
1077
      %(and_offering)s
 
1078
;""" % {"login": _escape(login), "and_offering": and_offering,
 
1079
        "and_projectset_table": and_projectset_table}
 
1080
        if dry:
 
1081
            return query
 
1082
        # Convert 't' -> True, 'f' -> False
 
1083
        return [(groupid, groupnm, nick, ismember == 't')
 
1084
                for groupid, groupnm, nick, ismember
 
1085
                in self.db.query(query).getresult()]
 
1086
 
 
1087
    def get_offering_info(self, projectsetid, dry=False):
 
1088
        """Takes information from projectset and returns useful information 
 
1089
        about the subject and semester. Returns as a dictionary.
 
1090
        """
 
1091
        query = """\
 
1092
SELECT subjectid, subj_code, subj_name, subj_short_name, url, year, semester, 
 
1093
active
 
1094
FROM subject, offering, semester, project_set
 
1095
WHERE offering.subject = subject.subjectid AND
 
1096
    offering.semesterid = semester.semesterid AND
 
1097
    project_set.offeringid = offering.offeringid AND
 
1098
    project_set.projectsetid = %d;"""%projectsetid
 
1099
        if dry:
 
1100
            return query
 
1101
        return self.db.query(query).dictresult()[0]
 
1102
 
 
1103
    def get_projectgroup_members(self, groupid, dry=False):
 
1104
        """Returns the logins of all students in a project group
 
1105
        """
 
1106
        query = """\
 
1107
SELECT login.login as login, login.fullname as fullname
 
1108
FROM login, group_member
 
1109
WHERE login.loginid = group_member.loginid AND
 
1110
    group_member.groupid = %d
 
1111
ORDER BY login.login;"""%groupid
 
1112
        if dry:
 
1113
            return query
 
1114
        return self.db.query(query).dictresult()
 
1115
 
 
1116
    def get_projectsets_by_offering(self, offeringid, dry=False):
 
1117
        """Returns all the projectsets in a particular offering"""
 
1118
        query = """\
 
1119
SELECT projectsetid, max_students_per_group
 
1120
FROM project_set
 
1121
WHERE project_set.offeringid = %d;"""%offeringid
 
1122
        if dry:
 
1123
            return query
 
1124
        return self.db.query(query).dictresult()
 
1125
 
 
1126
    def get_groups_by_projectset(self, projectsetid, dry=False):
 
1127
        """Returns all the groups that are in a particular projectset"""
 
1128
        query = """\
 
1129
SELECT groupid, groupnm, nick, createdby, epoch
 
1130
FROM project_group
 
1131
WHERE project_group.projectsetid = %d;"""%projectsetid
 
1132
        if dry:
 
1133
            return query
 
1134
        return self.db.query(query).dictresult()
 
1135
 
601
1136
    def close(self):
602
1137
        """Close the DB connection. Do not call any other functions after
603
1138
        this. (The behaviour of doing so is undefined).