Changeset 81

Show
Ignore:
Timestamp:
09/18/07 00:10:03 (1 year ago)
Author:
pragma
Message:
  • Improved ORM behavior for batch updates.
  • Finished Profile Avatar and Bio editing (watch mgmt is next)
  • Removed legacy code from Avatar
  • Took note of broken search and timeline
  • Updated published schema for postgres (is very raw, so beware)
Files:

Legend:

Unmodified
Added
Removed
Modified
Copied
Moved
  • trunk/sql/postgres.sql

    r71 r81  
    1 -- Sequence: avatar_id_seq 
    2  
    3 DROP SEQUENCE avatar_id_seq; 
     1-- 
     2-- PostgreSQL database dump 
     3-- 
     4 
     5-- Started on 2007-09-16 23:41:07 Eastern Daylight Time 
     6 
     7SET client_encoding = 'UTF8'; 
     8SET check_function_bodies = false; 
     9SET client_min_messages = warning; 
     10 
     11-- 
     12-- TOC entry 1612 (class 0 OID 0) 
     13-- Dependencies: 4 
     14-- Name: SCHEMA public; Type: COMMENT; Schema: -; Owner: postgres 
     15-- 
     16 
     17COMMENT ON SCHEMA public IS 'Standard public schema'; 
     18 
     19 
     20SET search_path = public, pg_catalog; 
     21 
     22SET default_tablespace = ''; 
     23 
     24SET default_with_oids = false; 
     25 
     26-- 
     27-- TOC entry 1233 (class 1259 OID 20723) 
     28-- Dependencies: 1592 4 
     29-- Name: avatar; Type: TABLE; Schema: public; Owner: postgres; Tablespace:  
     30-- 
     31 
     32CREATE TABLE avatar ( 
     33    username character varying(32), 
     34    mimetype character varying(256), 
     35    name text, 
     36    id integer DEFAULT nextval(('avatar_id_seq'::text)::regclass) 
     37); 
     38 
     39 
     40ALTER TABLE public.avatar OWNER TO postgres; 
     41 
     42-- 
     43-- TOC entry 1234 (class 1259 OID 20729) 
     44-- Dependencies: 4 
     45-- Name: avatar_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres 
     46-- 
    447 
    548CREATE SEQUENCE avatar_id_seq 
    6   INCREMENT 1 
    7   MINVALUE 1 
    8   MAXVALUE 9223372036854775807 
    9   START 1 
    10   CACHE 1; 
    11  
    12 -- Sequence: forum_id_seq 
    13  
    14 DROP SEQUENCE forum_id_seq; 
     49    INCREMENT BY 1 
     50    NO MAXVALUE 
     51    NO MINVALUE 
     52    CACHE 1; 
     53 
     54 
     55ALTER TABLE public.avatar_id_seq OWNER TO postgres; 
     56 
     57-- 
     58-- TOC entry 1228 (class 1259 OID 20679) 
     59-- Dependencies: 1569 1570 1571 1572 4 
     60-- Name: forum; Type: TABLE; Schema: public; Owner: postgres; Tablespace:  
     61-- 
     62 
     63CREATE TABLE forum ( 
     64    id integer DEFAULT nextval(('forum_id_seq'::text)::regclass) NOT NULL, 
     65    name text, 
     66    created integer, 
     67    modified integer, 
     68    description text, 
     69    projectid character varying(32), 
     70    locked boolean DEFAULT false, 
     71    hidden boolean DEFAULT false, 
     72    categoryid integer DEFAULT 0, 
     73    rank serial NOT NULL 
     74); 
     75 
     76 
     77ALTER TABLE public.forum OWNER TO postgres; 
     78 
     79-- 
     80-- TOC entry 1243 (class 1259 OID 1115442) 
     81-- Dependencies: 4 
     82-- Name: forum_category; Type: TABLE; Schema: public; Owner: postgres; Tablespace:  
     83-- 
     84 
     85CREATE TABLE forum_category ( 
     86    id serial NOT NULL, 
     87    projectid character varying(32), 
     88    description text, 
     89    rank serial NOT NULL 
     90); 
     91 
     92 
     93ALTER TABLE public.forum_category OWNER TO postgres; 
     94 
     95-- 
     96-- TOC entry 1226 (class 1259 OID 20675) 
     97-- Dependencies: 4 
     98-- Name: forum_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres 
     99-- 
    15100 
    16101CREATE SEQUENCE forum_id_seq 
    17   INCREMENT 1 
    18   MINVALUE 1 
    19   MAXVALUE 9223372036854775807 
    20   START 1 
    21   CACHE 1; 
    22  
    23 -- Sequence: message_id_seq 
    24  
    25 DROP SEQUENCE message_id_seq; 
     102    INCREMENT BY 1 
     103    NO MAXVALUE 
     104    NO MINVALUE 
     105    CACHE 1; 
     106 
     107 
     108ALTER TABLE public.forum_id_seq OWNER TO postgres; 
     109 
     110-- 
     111-- TOC entry 1237 (class 1259 OID 1115372) 
     112-- Dependencies: 4 
     113-- Name: forum_watch; Type: TABLE; Schema: public; Owner: postgres; Tablespace:  
     114-- 
     115 
     116CREATE TABLE forum_watch ( 
     117    id serial NOT NULL, 
     118    forumid integer, 
     119    username text 
     120); 
     121 
     122 
     123ALTER TABLE public.forum_watch OWNER TO postgres; 
     124 
     125-- 
     126-- TOC entry 1229 (class 1259 OID 20689) 
     127-- Dependencies: 1574 1575 1576 1577 4 
     128-- Name: message; Type: TABLE; Schema: public; Owner: postgres; Tablespace:  
     129-- 
     130 
     131CREATE TABLE message ( 
     132    id integer DEFAULT nextval(('message_id_seq'::text)::regclass) NOT NULL, 
     133    topicid integer, 
     134    created integer, 
     135    modified integer DEFAULT 0, 
     136    author text, 
     137    body text, 
     138    avatarid integer DEFAULT 0, 
     139    modifiedby character varying(32), 
     140    modcount integer DEFAULT 0 
     141); 
     142 
     143 
     144ALTER TABLE public.message OWNER TO postgres; 
     145 
     146-- 
     147-- TOC entry 1225 (class 1259 OID 20673) 
     148-- Dependencies: 4 
     149-- Name: message_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres 
     150-- 
    26151 
    27152CREATE SEQUENCE message_id_seq 
    28   INCREMENT 1 
    29   MINVALUE 1 
    30   MAXVALUE 9223372036854775807 
    31   START 1 
    32   CACHE 1; 
    33  
    34 -- Sequence: profile_id_seq 
    35  
    36  
    37 DROP SEQUENCE profile_id_seq; 
     153    INCREMENT BY 1 
     154    NO MAXVALUE 
     155    NO MINVALUE 
     156    CACHE 1; 
     157 
     158 
     159ALTER TABLE public.message_id_seq OWNER TO postgres; 
     160 
     161-- 
     162-- TOC entry 1232 (class 1259 OID 20719) 
     163-- Dependencies: 4 
     164-- Name: moderators; Type: TABLE; Schema: public; Owner: postgres; Tablespace:  
     165-- 
     166 
     167CREATE TABLE moderators ( 
     168    forumid integer NOT NULL, 
     169    username character varying(25) NOT NULL 
     170); 
     171 
     172 
     173ALTER TABLE public.moderators OWNER TO postgres; 
     174 
     175-- 
     176-- TOC entry 1231 (class 1259 OID 20705) 
     177-- Dependencies: 1582 1583 1584 1585 1586 1587 1588 1589 1590 1591 4 
     178-- Name: profile; Type: TABLE; Schema: public; Owner: postgres; Tablespace:  
     179-- 
     180 
     181CREATE TABLE profile ( 
     182    id integer DEFAULT nextval(('profile_id_seq'::text)::regclass) NOT NULL, 
     183    sig text DEFAULT ''::text, 
     184    username character varying(25) NOT NULL, 
     185    email character varying(255), 
     186    regdate integer DEFAULT 0 NOT NULL, 
     187    lastvisit integer DEFAULT 0 NOT NULL, 
     188    defaultavatarid integer DEFAULT 0, 
     189    bio text, 
     190    logindate integer DEFAULT 0, 
     191    timezone character varying(8), 
     192    viewemail boolean DEFAULT true, 
     193    isactive boolean DEFAULT false, 
     194    isexpert boolean DEFAULT false, 
     195    posts integer DEFAULT 0 
     196); 
     197 
     198 
     199ALTER TABLE public.profile OWNER TO postgres; 
     200 
     201-- 
     202-- TOC entry 1227 (class 1259 OID 20677) 
     203-- Dependencies: 4 
     204-- Name: profile_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres 
     205-- 
    38206 
    39207CREATE SEQUENCE profile_id_seq 
    40   INCREMENT 1 
    41   MINVALUE 1 
    42   MAXVALUE 9223372036854775807 
    43   START 1 
    44   CACHE 1; 
    45  
    46  
    47 -- Sequence: topic_id_seq 
    48  
    49 DROP SEQUENCE topic_id_seq; 
     208    INCREMENT BY 1 
     209    NO MAXVALUE 
     210    NO MINVALUE 
     211    CACHE 1; 
     212 
     213 
     214ALTER TABLE public.profile_id_seq OWNER TO postgres; 
     215 
     216-- 
     217-- TOC entry 1235 (class 1259 OID 1106501) 
     218-- Dependencies: 1593 4 
     219-- Name: project; Type: TABLE; Schema: public; Owner: postgres; Tablespace:  
     220-- 
     221 
     222CREATE TABLE project ( 
     223    id character varying(32) NOT NULL, 
     224    name text NOT NULL, 
     225    active boolean DEFAULT false NOT NULL 
     226); 
     227 
     228 
     229ALTER TABLE public.project OWNER TO postgres; 
     230 
     231-- 
     232-- TOC entry 1241 (class 1259 OID 1115394) 
     233-- Dependencies: 4 
     234-- Name: project_watch; Type: TABLE; Schema: public; Owner: postgres; Tablespace:  
     235-- 
     236 
     237CREATE TABLE project_watch ( 
     238    id serial NOT NULL, 
     239    username text, 
     240    projectid character varying(32) 
     241); 
     242 
     243 
     244ALTER TABLE public.project_watch OWNER TO postgres; 
     245 
     246-- 
     247-- TOC entry 1230 (class 1259 OID 20697) 
     248-- Dependencies: 1578 1579 1580 1581 4 
     249-- Name: topic; Type: TABLE; Schema: public; Owner: postgres; Tablespace:  
     250-- 
     251 
     252CREATE TABLE topic ( 
     253    id integer DEFAULT nextval(('topic_id_seq'::text)::regclass) NOT NULL, 
     254    forumid integer, 
     255    subject text, 
     256    leadmessageid integer DEFAULT 0, 
     257    views integer DEFAULT 0, 
     258    "type" character varying(16) DEFAULT ''::character varying 
     259); 
     260 
     261 
     262ALTER TABLE public.topic OWNER TO postgres; 
     263 
     264-- 
     265-- TOC entry 1224 (class 1259 OID 20671) 
     266-- Dependencies: 4 
     267-- Name: topic_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres 
     268-- 
    50269 
    51270CREATE SEQUENCE topic_id_seq 
    52   INCREMENT 1 
    53   MINVALUE 1 
    54   MAXVALUE 9223372036854775807 
    55   START 1 
    56   CACHE 1; 
    57  
    58 -- Table: avatar 
    59  
    60 DROP TABLE avatar; 
    61  
    62 CREATE TABLE avatar 
    63 
    64   username varchar(32), 
    65   mimetype varchar(256), 
    66   name text, 
    67   id int4 DEFAULT nextval(('avatar_id_seq'::text)::regclass) 
    68 )  
    69 WITHOUT OIDS; 
    70  
    71 -- Table: forum 
    72  
    73 DROP TABLE forum; 
    74  
    75 CREATE TABLE forum 
    76 
    77   id int4 NOT NULL DEFAULT nextval(('forum_id_seq'::text)::regclass), 
    78   name text, 
    79   created int4, 
    80   modified int4, 
    81   description text, 
    82   projectid varchar(32), 
    83   locked bool DEFAULT false, 
    84   hidden bool DEFAULT false, 
    85   categoryid int4 DEFAULT 0, 
    86   CONSTRAINT forum_pkey PRIMARY KEY (id), 
    87   CONSTRAINT forum_id_key UNIQUE (id, name) 
    88 )  
    89 WITHOUT OIDS; 
    90  
    91 -- Table: message 
    92  
    93 DROP TABLE message; 
    94  
    95 CREATE TABLE message 
    96 
    97   id int4 NOT NULL DEFAULT nextval(('message_id_seq'::text)::regclass), 
    98   topicid int4, 
    99   created int4, 
    100   modified int4 DEFAULT 0, 
    101   author text, 
    102   body text, 
    103   avatarid int4 DEFAULT 0, 
    104   modifiedby varchar(32), 
    105   modcount int4 DEFAULT 0, 
    106   CONSTRAINT message_pkey PRIMARY KEY (id) 
    107 )  
    108 WITHOUT OIDS; 
    109  
    110 -- Table: moderators 
    111  
    112 DROP TABLE moderators; 
    113  
    114 CREATE TABLE moderators 
    115 
    116   forumid int4 NOT NULL, 
    117   username varchar(25) NOT NULL 
    118 )  
    119 WITHOUT OIDS; 
    120  
    121 -- Table: profile 
    122  
    123  
    124 DROP TABLE profile; 
    125  
    126 CREATE TABLE profile 
    127 
    128   id int4 NOT NULL DEFAULT nextval(('profile_id_seq'::text)::regclass), 
    129   sig text DEFAULT ''::text, 
    130   username varchar(25) NOT NULL, 
    131   email varchar(255), 
    132   regdate int4 NOT NULL DEFAULT 0, 
    133   lastvisit int4 NOT NULL DEFAULT 0, 
    134   defaultavatarid int4 DEFAULT 0, 
    135   bio text, 
    136   logindate int4 DEFAULT 0, 
    137   timezone varchar(8), 
    138   viewemail bool DEFAULT true, 
    139   isactive bool DEFAULT false, 
    140   isexpert bool DEFAULT false, 
    141   posts int4 DEFAULT 0 
    142 )  
    143 WITHOUT OIDS; 
    144  
    145  
    146 -- Table: topic 
    147  
    148 DROP TABLE topic; 
    149  
    150 CREATE TABLE topic 
    151 
    152   id int4 NOT NULL DEFAULT nextval(('topic_id_seq'::text)::regclass), 
    153   forumid int4, 
    154   subject text, 
    155   leadmessageid int4 DEFAULT 0, 
    156   views int4 DEFAULT 0, 
    157   "type" varchar(16) DEFAULT ''::character varying, 
    158   CONSTRAINT topic_pkey PRIMARY KEY (id) 
    159 )  
    160 WITHOUT OIDS; 
    161  
    162 -- Table: project 
    163  
    164 DROP TABLE project; 
    165  
    166 CREATE TABLE project 
    167 
    168   id varchar(32) NOT NULL, 
    169   name text NOT NULL, 
    170   active bool NOT NULL DEFAULT false 
    171 )  
    172 WITHOUT OIDS; 
    173  
    174 -- Table: forum_watch 
    175  
    176 -- DROP TABLE forum_watch; 
    177  
    178 CREATE TABLE forum_watch 
    179 
    180   id serial NOT NULL, 
    181   forum_id int4, 
    182   username text 
    183 )  
    184 WITHOUT OIDS; 
    185 ALTER TABLE forum_watch OWNER TO postgres; 
    186  
    187 -- Table: topic_watch 
    188  
    189 -- DROP TABLE topic_watch; 
    190  
    191 CREATE TABLE topic_watch 
    192 
    193   id serial NOT NULL, 
    194   topicid int4, 
    195   username text 
    196 )  
    197 WITHOUT OIDS; 
    198 ALTER TABLE topic_watch OWNER TO postgres; 
    199  
    200 -- Table: project_watch 
    201  
    202 -- DROP TABLE project_watch; 
    203  
    204 CREATE TABLE project_watch 
    205 
    206   id serial NOT NULL, 
    207   username text, 
    208   projectid varchar(32) 
    209 )  
    210 WITHOUT OIDS; 
    211 ALTER TABLE project_watch OWNER TO postgres; 
    212  
    213 -- Table: forum_category 
    214  
    215 -- DROP TABLE forum_category; 
    216  
    217 CREATE TABLE forum_category 
    218 
    219   id serial NOT NULL, 
    220   projectid varchar(32), 
    221   description text, 
    222   sortorder int4 DEFAULT 0 
    223 )  
    224 WITHOUT OIDS; 
    225 ALTER TABLE forum_category OWNER TO postgres; 
     271    INCREMENT BY 1 
     272    NO MAXVALUE 
     273    NO MINVALUE 
     274    CACHE 1; 
     275 
     276 
     277ALTER TABLE public.topic_id_seq OWNER TO postgres; 
     278 
     279-- 
     280-- TOC entry 1239 (class 1259 OID 1115386) 
     281-- Dependencies: 4 
     282-- Name: topic_watch; Type: TABLE; Schema: public; Owner: postgres; Tablespace:  
     283-- 
     284 
     285CREATE TABLE topic_watch ( 
     286    id serial NOT NULL, 
     287    topicid integer, 
     288    username text 
     289); 
     290 
     291 
     292ALTER TABLE public.topic_watch OWNER TO postgres; 
     293 
     294-- 
     295-- TOC entry 1247 (class 1259 OID 1115554) 
     296-- Dependencies: 4 
     297-- Name: touched_topic; Type: TABLE; Schema: public; Owner: postgres; Tablespace:  
     298-- 
     299 
     300CREATE TABLE touched_topic ( 
     301    id serial NOT NULL, 
     302    username character varying(32), 
     303    topicid integer 
     304); 
     305 
     306 
     307ALTER TABLE public.touched_topic OWNER TO postgres; 
     308 
     309-- 
     310-- TOC entry 1222 (class 1259 OID 20364) 
     311-- Dependencies: 4 
     312-- Name: trac_cookies; Type: TABLE; Schema: public; Owner: postgres; Tablespace:  
     313-- 
     314 
     315CREATE TABLE trac_cookies ( 
     316    envname text NOT NULL, 
     317    cookie text NOT NULL, 
     318    username text NOT NULL, 
     319    ipnr text NOT NULL, 
     320    unixtime integer NOT NULL 
     321); 
     322 
     323 
     324ALTER TABLE public.trac_cookies OWNER TO postgres; 
     325 
     326-- 
     327-- TOC entry 1223 (class 1259 OID 20369) 
     328-- Dependencies: 4 
     329-- Name: trac_permissions; Type: TABLE; Schema: public; Owner: postgres; Tablespace:  
     330-- 
     331 
     332CREATE TABLE trac_permissions ( 
     333    envname text NOT NULL, 
     334    username text NOT NULL, 
     335    groupname text NOT NULL 
     336); 
     337 
     338 
     339ALTER TABLE public.trac_permissions OWNER TO postgres; 
     340 
     341-- 
     342-- TOC entry 1221 (class 1259 OID 20357) 
     343-- Dependencies: 4 
     344-- Name: trac_users; Type: TABLE; Schema: public; Owner: postgres; Tablespace:  
     345-- 
     346 
     347CREATE TABLE trac_users ( 
     348    envname text NOT NULL, 
     349    username text NOT NULL, 
     350    "password" text NOT NULL, 
     351    email text 
     352); 
     353 
     354 
     355ALTER TABLE public.trac_users OWNER TO postgres; 
     356 
     357-- 
     358-- TOC entry 1603 (class 2606 OID 20688) 
     359-- Dependencies: 1228 1228 1228 
     360-- Name: forum_id_key; Type: CONSTRAINT; Schema: public; Owner: postgres; Tablespace:  
     361-- 
     362 
     363ALTER TABLE ONLY forum 
     364    ADD CONSTRAINT forum_id_key UNIQUE (id, name); 
     365 
     366 
     367-- 
     368-- TOC entry 1605 (class 2606 OID 20686) 
     369-- Dependencies: 1228 1228 
     370-- Name: forum_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres; Tablespace:  
     371-- 
     372 
     373ALTER TABLE ONLY forum 
     374    ADD CONSTRAINT forum_pkey PRIMARY KEY (id); 
     375 
     376 
     377-- 
     378-- TOC entry 1608 (class 2606 OID 20696) 
     379-- Dependencies: 1229 1229 
     380-- Name: message_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres; Tablespace:  
     381-- 
     382 
     383ALTER TABLE ONLY message 
     384    ADD CONSTRAINT message_pkey PRIMARY KEY (id); 
     385 
     386 
     387-- 
     388-- TOC entry 1610 (class 2606 OID 20704) 
     389-- Dependencies: 1230 1230 
     390-- Name: topic_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres; Tablespace:  
     391-- 
     392 
     393ALTER TABLE ONLY topic 
     394    ADD CONSTRAINT topic_pkey PRIMARY KEY (id); 
     395 
     396 
     397-- 
     398-- TOC entry 1601 (class 2606 OID 20363) 
     399-- Dependencies: 1221 1221 1221 
     400-- Name: trac_users_envname_key; Type: CONSTRAINT; Schema: public; Owner: postgres; Tablespace:  
     401-- 
     402 
     403ALTER TABLE ONLY trac_users 
     404    ADD CONSTRAINT trac_users_envname_key UNIQUE (envname, username); 
     405 
     406 
     407-- 
     408-- TOC entry 1606 (class 1259 OID 1097439) 
     409-- Dependencies: 1229 
     410-- Name: message_modified_idx; Type: INDEX; Schema: public; Owner: postgres; Tablespace:  
     411-- 
     412 
     413CREATE INDEX message_modified_idx ON message USING btree (modified); 
     414 
     415 
     416-- 
     417-- TOC entry 1613 (class 0 OID 0) 
     418-- Dependencies: 4 
     419-- Name: public; Type: ACL; Schema: -; Owner: postgres 
     420-- 
     421 
     422REVOKE ALL ON SCHEMA public FROM PUBLIC; 
     423REVOKE ALL ON SCHEMA public FROM postgres; 
     424GRANT ALL ON SCHEMA public TO postgres; 
     425GRANT ALL ON SCHEMA public TO PUBLIC; 
     426 
     427 
     428-- Completed on 2007-09-16 23:41:08 Eastern Daylight Time 
     429 
     430-- 
     431-- PostgreSQL database dump complete 
     432-- 
     433 
  • trunk/todo.txt

    r80 r81  
    99    * Document code 
    1010    * Complete console interface (?) 
     11    * Publish updated database schema 
    1112Macros: 
    1213    * Blog-style reports and displays 
     
    1617    * Potential security flaws in selection/move/delete interface  
    1718    * clamp "forum get" security to current project for non trac-admins, system wide 
    18 Refactor: 
    19     * reduce SQL redundancy in models 
    20     * reduce repeated code blocks in models     
    2119Wishlist: 
    2220    * RSS 
  • trunk/tracforums/model.py

    r80 r81  
    139139    def get(self,name,defaultValue=None): 
    140140        val = dict.get(self,name,defaultValue) 
    141         if not val: return defaultValue 
     141        if val == None: return defaultValue 
    142142        return val 
    143143         
     
    219219    def getProfilePath(self): 
    220220        return self.env.config.get("tracforums","profilepath",'/') 
    221  
     221         
     222    def getAvatarMetrics(self): 
     223        return { 
     224            "number": self.env.config.get("tracforums","max_avatars",10), 
     225            "size": self.env.config.get("tracforums","max_avatar_bytes",15000), 
     226            "width": self.env.config.get("tracforums","max_avatar_width",100), 
     227            "height": self.env.config.get("tracforums","max_avatar_height",100)         
     228        } 
     229         
    222230    def forumRedirect(self,path,suffix=""): 
    223231        self.db.commit() 
  • trunk/tracforums/models/avatar.py

    r73 r81  
    1010    Avatar 
    1111""" 
    12 AvatarModel = ORMSchema( 
    13    tablename = "avatar", 
    14    columns   = { 
    15        "id": ORMKey(type='int', auto_increment = True), 
     12class AvatarModel(ORMSchema( 
     13    tablename = "avatar", 
     14    columns   = { 
     15        "id": ORMKey(type='int', auto_increment = True), 
    1616        "username":  ORMColumn(), 
    1717        "mimetype":  ORMColumn(), 
     
    1919        "avatarid":  ORMAlias(sql="id"), 
    2020    } 
    21 
    22  
    23 class AvatarController(Controller):         
     21)): 
    2422    mimeMap = { 
    25        'bmp':  'image/bmp', 
     23        'bmp':  'image/bmp', 
    2624        'cod':  'image/cis-cod', 
    27         'gif':  'image/gif',    
     25        'gif':  'image/gif',     
    2826        'ief':  'image/ief', 
    29         'jpe':  'image/jpeg',  
     27        'jpe':  'image/jpeg',    
    3028        'jpeg': 'image/jpeg', 
    3129        'jpg':  'image/jpeg', 
    32        'jfif': 'image/pipeg', 
    33         'svg':  'image/svg+xml',    
     30        'jfif': 'image/pipeg', 
     31        'svg':  'image/svg+xml',     
    3432        'tif':  'image/tiff', 
    3533        'tiff': 'image/tiff', 
     
    3836        'ico':  'image/x-icon', 
    3937        'png':  'iimage/png', 
    40        'pnm':  'image/x-portable-anymap', 
    41        'pbm':  'image/x-portable-bitmap', 
    42        'pgm':  'image/x-portable-graymap', 
    43         'ppm':  'image/x-portable-pixmap',         
     38        'pnm':  'image/x-portable-anymap', 
     39        'pbm':  'image/x-portable-bitmap', 
     40        'pgm':  'image/x-portable-graymap', 
     41        'ppm':  'image/x-portable-pixmap',           
    4442        'rgb':  'image/x-rgb', 
    4543        'xbm':  'image/x-xbitmap', 
    4644        'xpm':  'image/x-xpixmap', 
    47        'xwd':  'image/x-xwindowdump'  
     45        'xwd':  'image/x-xwindowdump'  
    4846    } 
    4947     
     48    def save(self,data={}): 
     49        self.getBase().save(self,data) 
     50         
     51        if "filedata" in self.__dict__: 
     52            #get the mime-type by file extension 
     53            suffix = self.filedata.filename.split('.')[-1]            
     54            self.mimetype = self.mimeMap[suffix] 
     55                 
     56            # store the file 
     57            flags = os.O_CREAT + os.O_WRONLY + os.O_EXCL 
     58            if hasattr(os, 'O_BINARY'): 
     59                flags += os.O_BINARY 
     60                             
     61            targetPath = self.getTargetPath() 
     62            try: # Q&D way to clear out the file if it's already there 
     63                os.remove(targetPath) 
     64            except Exception,e: pass 
     65             
     66            targetFile = os.fdopen(os.open(targetPath, flags), 'w') 
     67                     
     68            shutil.copyfileobj(self.filedata.file,targetFile) 
     69            targetFile.close() 
     70             
     71            # save again - forces the ORM to update this record with the mimetype 
     72            self.getBase().save(self)  
     73     
     74    def delete(self,data={}): 
     75        self.getBase().delete(self,data) 
     76             
     77        # reset default any associated profile avatar ids if set 
     78        from tracforums.models.profile import ProfileModel 
     79        ProfileModel(self.db,self.formatContext).updateMany({"defaultavatarid":self.id},{"defaultavatarid":0}) 
     80         
     81        from tracforums.models.message import MessageModel 
     82        ProfileModel(self.db,self.formatContext).updateMany({"avatarid":self.id},{"avatarid":0}) 
     83             
     84        # remove avatar image 
     85        os.remove(self.getTargetPath()) 
     86     
     87    def validate(self): 
     88        reasons = [] 
     89         
     90        if self.username == None or self.username == "": 
     91            reasons.append("The avatar username is required.") 
     92   
     93        # check if the file has been uploaded                 
     94        if "filedata" in self.__dict__: 
     95            print "FILE:",self.filedata 
     96            metrics = self.formatContext.getAvatarMetrics() 
     97             
     98            if not hasattr(self.filedata, 'filename') or not self.filedata.filename: 
     99                reasons.append("No file uploaded.") 
     100                return reasons 
     101            # check the file size                
     102                 
     103            if hasattr(self.filedata.file, 'fileno'): 
     104                size = os.fstat(self.filedata.file.fileno())[6] 
     105            else: 
     106                size = self.filedata.file.len 
     107             
     108            if size == 0: 
     109                reasons.append("Can't upload empty file") 
     110                return reasons 
     111            elif size >= metrics["size"]: 
     112                reasons.append("File size cannot exceed %s bytes" % metrics["size"]) 
     113                return reasons 
     114                             
     115            # check the mime type     
     116            suffix = self.filedata.filename.split('.')[-1] 
     117            if not (suffix in self.mimeMap): 
     118                reasons.append("Unrecognized file type: '" + suffix + "'") 
     119                return reasons 
     120                 
     121            # check the file dimensions (via PIL) 
     122            import Image 
     123             
     124            img = Image.open(self.filedata.file) 
     125            (width,height) = img.size 
     126            if height > metrics["height"] or width > metrics["width"]: 
     127                reasons.append("File exceeds allowed width and or height (%spx high by %spx wide )" % (metrics["height"],metrics["width"])) 
     128                 
     129            self.filedata.file.seek(0) 
     130        return reasons 
     131     
     132    def validateCreate(self): 
     133        reasons = self.validate()  
     134         
     135        totalAvatars = len(self.getColumnValues("id")) 
     136        metrics = self.formatContext.getAvatarMetrics() 
     137        if totalAvatars > metrics["number"]: 
     138            reasons.append("This user already has the maximum number of allowed avatars (%s)." % metrics.number) 
     139  
     140        return reasons 
     141           
     142    def validateSave(self): 
     143        reasons = self.validate() 
     144        return reasons 
     145     
     146    def getTargetPath(self): 
     147        # get the target path 
     148        targetPath = os.path.join(self.formatContext.getProfilePath(),'avatar') 
     149         
     150        # make the directory if it's not there 
     151        if not os.access(targetPath, os.F_OK): 
     152            os.makedirs(targetPath) 
     153         
     154        # extend the path to point to the correct file 
     155        targetPath = os.path.join(targetPath,str(self.id) + ".data") 
     156         
     157        return targetPath         
     158 
     159AvatarModelWithDetails = ORMSchema( 
     160    base = AvatarModel, 
     161    columns = { 
     162        "usedcount" : ORMAlias(sql=""" 
     163            select count(m.id) 
     164            from message as m  
     165            where m.avatarid = avatar.id 
     166        """), 
     167        "recentpostid" : ORMAlias(sql=""" 
     168            select max(m.id)  
     169            from message as m  
     170            where m.avatarid = avatar.id 
     171        """), 
     172        "recentpost":  ORMJoin( 
     173            model         = ORMImportModel("tracforums.models.message","MessageModel"), 
     174            relationship  = {"recentpostid":"messageid"} 
     175        ) 
     176    } 
     177) 
     178 
     179class AvatarController(Controller):             
    50180    def __init__(self,req,env,db): 
    51181        Controller.__init__(self,req,env,db) 
     
    54184        args = self.getArgs() 
    55185        avatar = AvatarModel(self.db,self).load({"id":args["viewid"]}) 
    56          
    57         # get the target path 
    58         targetPath = os.path.join(args["profilepath"],'avatar') 
    59          
    60         # make the directory if it's not there 
    61         if not os.access(targetPath, os.F_OK): 
    62             os.makedirs(targetPath) 
    63          
    64         # extend the path to point to the correct file 
    65         targetPath = os.path.join(targetPath,str(avatar.id) + ".data")   
    66                
    67         # render 
    68         self.renderAsFile(targetPath,avatar.mimetype) 
     186 
     187        self.renderAsFile(avatar.getTargetPath(),avatar.mimetype) 
    69188        return "", None         
    70189         
     
    73192        "default": doView 
    74193    } 
    75  
    76 #################################################### 
    77  
    78  
    79 class Avatar(ISecurityContext,IModel,IUIModel): 
    80     def __init__(self,db): 
    81         self.db = db 
    82         self.instance = {} 
    83          
    84     def _getTargetPath(self): 
    85         # get the target path 
    86         targetPath = os.path.join(self.instance["profilepath"],'avatar') 
    87          
    88         # make the directory if it's not there 
    89         if not os.access(targetPath, os.F_OK): 
    90             os.makedirs(targetPath) 
    91          
    92         # extend the path to point to the correct file 
    93         targetPath = os.path.join(targetPath,str(self.instance["id"]) + ".data") 
    94         return targetPath 
    95          
    96     mimeMap = { 
    97         'bmp':  'image/bmp', 
    98         'cod':  'image/cis-cod', 
    99         'gif':  'image/gif',     
    100         'ief':  'image/ief', 
    101         'jpe':  'image/jpeg',    
    102         'jpeg': 'image/jpeg', 
    103         'jpg':  'image/jpeg', 
    104         'jfif': 'image/pipeg', 
    105         'svg':  'image/svg+xml',     
    106         'tif':  'image/tiff', 
    107         'tiff': 'image/tiff', 
    108         'ras':  'image/x-cmu-raster', 
    109         'cmx':  'image/x-cmx', 
    110         'ico':  'image/x-icon', 
    111         'png':  'iimage/png', 
    112         'pnm':  'image/x-portable-anymap', 
    113         'pbm':  'image/x-portable-bitmap', 
    114         'pgm':  'image/x-portable-graymap', 
    115         'ppm':  'image/x-portable-pixmap',           
    116         'rgb':  'image/x-rgb', 
    117         'xbm':  'image/x-xbitmap', 
    118         'xpm':  'image/x-xpixmap', 
    119         'xwd':  'image/x-xwindowdump'  
    120     } 
    121                  
    122     ### IUIModel Methods ### 
    123      
    124     def _getAssets(self,context): 
    125         assets = {} 
    126         self.instance['filedata'] = '' # strike filedata from display set 
    127         assets['avatar'] = self.instance 
    128         return assets 
    129  
    130     def getViewTemplate(self,context): 
    131         context.renderAsFile(self._getTargetPath(),self.instance["mimetype"]) 
    132         return "", None 
    133          
    134     def getEditTemplate(self,context): 
    135         return "avatar/edit.cs",self._getAssets(context) 
    136          
    137     def getSaveTemplate(self,context):         
    138         from tracforums.models.profile import Profile 
    139         profile = Profile(self.db).getById(context.getAuthname(),context) 
    140          
    141         if profile['isexpert']: 
    142             context.forumRedirect("/profile/" + str(self.instance['username'])); 
    143         else:     
    144             return "avatar/save.cs",self._getAssets(context) 
    145          
    146     def getNewTemplate(self,context): 
    147         return self.getEditTemplate(context) 
    148          
    149     def getCreateTemplate(self,context): 
    150         return self.getSaveTemplate(context) 
    151  
    152     def getDeletedTemplate(self,context): 
    153         return self.getSaveTemplate(context) 
    154  
    155     ### ISecurityContext Methods ### 
    156          
    157     def canView(self,context): 
    158         return context.isForumGuest() 
    159                 
    160     def canCreate(self,context): 
    161         return context.isForumUser() 
    162                      
    163     def canModify(self,context): 
    164         if context.isForumAdmin(): 
    165             return True 
    166         else: 
    167             return context.getAuthname() == self.instance['username'] 
    168          
    169     def canDelete(self,context): 
    170         if context.isForumAdmin(): 
    171             return True 
    172         else: 
    173             return context.getAuthname() == self.instance['username'] 
    174  
    175     ### IModel Methods ### 
    176      
    177     def load(self,context): 
    178         args = context.getArgs() 
    179         if "id" in args: 
    180             id = args["id"] 
    181         else: 
    182             raise TracError("cannot load profile avatar") 
    183              
    184         # load instance (if possible) 
    185         cursor = self.db.cursor() 
    186         columns = ('id','username','mimetype','name') 
    187         cursor.execute( 
    188             "SELECT id, username, mimetype, name " 
    189             "FROM avatar WHERE id = %s", 
    190             (id,)) 
    191         row = cursor.fetchone() 
    192         self.instance = dict(zip(columns, row)) 
    193         self.instance["profilepath"] = args["profilepath"] 
    194                      
    195     def set(self,context): 
    196         args = context.getArgs() 
    197         # populate instance with changes and fresh values  
    198         self.instance = { 
    199             'id':          self.getDefault((args,self.instance),'id',''), 
    200             'username':    self.getDefault((args,self.instance),'username',''), 
    201             'mimetype':    self.getDefault((args,self.instance),'mimetype',''), 
    202             'name':        self.getDefault((args,self.instance),'name',''), 
    203             'profilepath': args["profilepath"], 
    204             'filedata':    self.getDefault((args,self.instance),'filedata',{}) 
    205         } 
    206                  
    207     def _saveAvatarImage(self,context): 
    208         # store the file 
    209         flags = os.O_CREAT + os.O_WRONLY + os.O_EXCL 
    210         if hasattr(os, 'O_BINARY'): 
    211             flags += os.O_BINARY 
    212              
    213         targetPath = self._getTargetPath() 
    214         targetFile = os.fdopen(os.open(targetPath, flags), 'w') 
    215                  
    216         shutil.copyfileobj(self.instance['filedata'].file,targetFile) 
    217         targetFile.close() 
    218          
    219     def _setMimeType(self,context): 
    220         #get the mime-type by file extension 
    221         filedata = self.instance['filedata'] 
    222         suffix = filedata.filename.split('.')[-1]            
    223         self.instance['mimetype'] = self.mimeMap[suffix] 
    224          
    225     def create(self,context): 
    226         self._setMimeType(context) 
    227      
    228         cursor = self.db.cursor() 
    229         cursor.execute( 
    230             "INSERT INTO avatar (username,mimetype,name) " 
    231             "VALUES (%(username)s, %(mimetype)s, %(name)s)", 
    232             self.instance) 
    233         self.instance["id"] = self.db.get_last_id(cursor, 'avatar') 
    234          
    235         self._saveAvatarImage(context)               
    236          
    237     def save(self,context): 
    238         # do file stuff if the file was changed 
    239         filedata = self.instance["filedata"] 
    240         if not hasattr(filedata, 'filename') or not filedata.filename: 
    241             self._saveAvatarImage(context) 
    242             self._setMimeType(context) 
    243          
    244         # update database 
    245         cursor = self.db.cursor() 
    246         cursor.execute( 
    247             "UPDATE avatar " 
    248             "SET name=%(name)s, mimetype=%(mimetype)s " 
    249             "WHERE id=%(id)s ", 
    250             self.instance)        
    251                
    252     def delete(self,context): 
    253         cursor = self.db.cursor() 
    254         cursor.execute( 
    255             "DELETE FROM avatar WHERE id=%(id)s", 
    256             self.instance) 
    257              
    258         # reset default profile avatar id if set 
    259         cursor.execute( 
    260             "UPDATE profile SET defaultavatarid = 0 WHERE profile.defaultavatarid = %(id)s", 
    261             self.instance) 
    262              
    263         # remove avatar image 
    264         os.remove(self._getTargetPath()) 
    265          
    266     def _validate(self,context): 
    267         reasons = [] 
    268          
    269         if self.instance['username'] == "": 
    270             reasons.append("The avatar username is required.") 
    271              
    272         if self.instance['name'] == "": 
    273             reasons.append("The avatar name is required.")             
    274                          
    275         return reasons 
    276              
    277     def validateCreate(self,context): 
    278         reasons = self._validate(context) 
    279         cursor = self.db.cursor()       
    280              
    281         cursor.execute("SELECT count(id) FROM avatar WHERE username=%(username)s",self.instance) 
    282         row = cursor.fetchone() 
    283         if row and row[0] > 10: #TODO: use a configurable value 
    284             reasons.append("This user already has the maximum number of allowed avatars.") 
    285             return reasons 
    286     
    287         # check if the file has been uploaded 
    288         filedata = self.instance["filedata"] 
    289          
    290         if not hasattr(filedata, 'filename') or not filedata.filename: 
    291             reasons.append("No file uploaded") 
    292             return reasons 
    293