PageRenderTime 29ms CodeModel.GetById 12ms RepoModel.GetById 0ms app.codeStats 0ms

/demisauce/demisauce/model/user.py

https://github.com/araddon/demisauce
Python | 591 lines | 546 code | 16 blank | 29 comment | 7 complexity | e5ef59d0d4ab625f75aa84ed0173a1f9 MD5 | raw file
  1. import logging, json
  2. import string
  3. from random import choice
  4. from Crypto.Cipher import AES
  5. from sqlalchemy import Column, MetaData, ForeignKey, Table, \
  6. func, UniqueConstraint
  7. from sqlalchemy import Integer, String as DBString, DateTime, Boolean, \
  8. Text as DBText
  9. from sqlalchemy import engine, orm
  10. from sqlalchemy.orm import mapper, relation, class_mapper, synonym, dynamic_loader
  11. from sqlalchemy.sql import and_, text
  12. from datetime import datetime
  13. import formencode
  14. import random, hashlib, string
  15. from wtforms import Form, BooleanField, TextField, TextAreaField, \
  16. PasswordField, SelectField, SelectMultipleField, HiddenField, \
  17. IntegerField, validators
  18. from wtforms.validators import ValidationError
  19. from tornado import escape
  20. from tornado.options import options
  21. from demisauce import model
  22. from demisauce.model import meta
  23. from demisauce.model.site import Site
  24. from demisauce.model.activity import Activity
  25. from demisauce.model import ModelBase, SerializationMixin
  26. from datetime import datetime
  27. log = logging.getLogger("demisauce")
  28. # Person
  29. person_table = Table("person", meta.metadata,
  30. Column("id", Integer, primary_key=True),
  31. Column("site_id", Integer, ForeignKey('site.id')),
  32. Column("foreign_id", Integer, default=0, index=True),
  33. Column("email", DBString(255)),
  34. Column("displayname", DBString(50)),
  35. Column("created", DateTime,default=datetime.now),
  36. Column("last_login", DateTime),
  37. Column("waitinglist", Integer, default=1),
  38. Column("verified", Boolean, default=False),
  39. Column("isadmin", Boolean, default=False),
  40. Column("issysadmin", Boolean, default=False),
  41. Column("user_uniqueid", DBString(40)),
  42. Column("authn", DBString(8),default='local'),
  43. Column("hashedemail", DBString(32), nullable=True),
  44. Column("url", DBString(255),default="http://yourapp.wordpress.com",nullable=False),
  45. Column("random_salt", DBString(120)),
  46. Column("hashed_password", DBString(120)),
  47. Column("password", DBString(60)),
  48. Column("extra_json", DBText),
  49. UniqueConstraint('hashedemail','site_id'),
  50. )
  51. # DB Table: group ---------------------------
  52. group_table = Table("group", meta.metadata,
  53. Column("id", Integer, primary_key=True),
  54. Column("site_id", Integer, ForeignKey('site.id')),
  55. Column("author_id", Integer, nullable=True,default=0),
  56. Column("created", DateTime,default=datetime.now),
  57. Column("group_type", DBString(30), default='groups'),
  58. Column("name", DBString(255), nullable=False),
  59. Column("slug", DBString(150), nullable=False),
  60. Column("description", DBString(500)),
  61. Column("contacts", DBText),
  62. Column("public", Boolean, default=False),
  63. Column("extra_json", DBText),
  64. )
  65. userattribute_table = Table('userattribute', meta.metadata,
  66. Column('id', Integer, primary_key=True),
  67. Column('person_id', Integer, ForeignKey('person.id')),
  68. Column('object_id', Integer,default=0),
  69. Column('object_type', DBString(30)),
  70. Column('name', DBString(80)),
  71. Column("value", DBString(1000)),
  72. Column('category', DBString(30)),
  73. Column('encoding', DBString(10),default="str"),
  74. Column("created", DateTime,default=datetime.now),
  75. )
  76. class SignupForm(Form):
  77. def validate_email(form, field):
  78. f = meta.DBSession().query(Person).filter(Person.email == field.data).first()
  79. if f:
  80. log.debug("f.email=%s, field.data=%s" % (f.email,field.data))
  81. raise ValidationError(u'That Email is already in use, choose another')
  82. email = TextField('Email', [validators.Email()])
  83. class InviteForm(Form):
  84. password = PasswordField('New Password')
  85. password2 = PasswordField('Confirm Password', [validators.Required(), validators.EqualTo('password', message='Passwords must match')])
  86. displayname = TextField('Display Name')
  87. sitename = TextField('Name of your site')
  88. class GroupForm(Form):
  89. "Form validation for the comment web admin"
  90. name = TextField('Name of your Group')
  91. members = TextField('list of members')
  92. from formencode import Invalid, validators
  93. from formencode.validators import *
  94. class UniqueEmail(formencode.FancyValidator):
  95. def _to_python(self, value, state):
  96. #raise formencode.Invalid('Sorry, this is hosed', value, state)
  97. user = meta.DBSession.query(Person).filter_by(email=value.lower()).first()
  98. # We can use user[0] because if the user didn't exist this would not be called
  99. if user:
  100. raise formencode.Invalid('that email is already registered', value, state)
  101. return value
  102. class InvitationIsValid(formencode.FancyValidator):
  103. def _to_python(self, value, state):
  104. user = meta.DBSession.query(Person).filter_by(
  105. user_uniqueid=value.lower()).first()
  106. if not user:
  107. raise formencode.Invalid('Invalid Invitation code', value, state)
  108. return value
  109. """
  110. class GuestValidation(formencode.Schema):
  111. allow_extra_fields = True
  112. filter_extra_fields = False
  113. email = formencode.All(validators.Email(resolve_domain=False),
  114. validators.String(not_empty=True),
  115. UniqueEmail())
  116. """
  117. class InviteValidation(formencode.Schema):
  118. allow_extra_fields = True
  119. filter_extra_fields = False
  120. #email = formencode.All(validators.Email(resolve_domain=False),
  121. # validators.String(not_empty=True))
  122. invitecode = formencode.All(InvitationIsValid())
  123. password = formencode.All(validators.NotEmpty(),validators.MinLength(5))
  124. password2 = formencode.All(validators.NotEmpty(),validators.MinLength(5))
  125. class PersonValidation(formencode.Schema):
  126. allow_extra_fields = True
  127. filter_extra_fields = False
  128. email = formencode.All(validators.Email(resolve_domain=False),
  129. validators.String(not_empty=True))
  130. password = formencode.All(validators.NotEmpty(),validators.MinLength(5))
  131. class PersonEditValidation(formencode.Schema):
  132. allow_extra_fields = True
  133. filter_extra_fields = False
  134. email = formencode.All(validators.Email(resolve_domain=False),
  135. validators.String(not_empty=True))
  136. displayname = formencode.All(validators.String(not_empty=True))
  137. class Person(ModelBase,SerializationMixin):
  138. """User/Person, base identity and user object
  139. :email: email of user
  140. :site_id: id of the site the current user belongs to
  141. :displayname: Full Name (first + last) of user (or whatever they enter)
  142. :created: date they joined
  143. :last_login: date they last logged on
  144. :hashedemail: md5 hashed email that is Gravatar_ format
  145. :url: url to blog or site of user
  146. :profile_url: url to json api
  147. :authn: local, google, openid, etc (which authN method to use)
  148. :user_uniqueid: uniqueid of user (random) for use in querystring's instead of id
  149. :foreign_id: id (number) of user within your system
  150. :issysadmin: is user a sysadmin (admin for all sites)
  151. :isadmin: is user an admin for current site
  152. .. _Gravatar: http://www.gravatar.com/
  153. """
  154. __jsonkeys__ = ['email','displayname','url','site_id', 'raw_password','created']
  155. _readonly_keys = ['id','hashedemail','profile_url']
  156. _api_keys = ['name','displayname','id','email','profile_url','url','hashedemail','foreign_id','authn','extra_json']
  157. _allowed_api_keys = ['isadmin','email','displayname','url','raw_password','authn','user_uniqueid','foreign_id','extra_json']
  158. schema = person_table
  159. def __init__(self, **kwargs):
  160. super(Person, self).__init__(**kwargs)
  161. self.after_load()
  162. self.init_on_load()
  163. @orm.reconstructor
  164. def init_on_load(self):
  165. "called by sq after load as init type"
  166. super(Person, self).init_on_load()
  167. self.session = {'testcrap':'crap'}
  168. @property
  169. def profile_url(self):
  170. if self.hashedemail:
  171. return "%s/api/person/%s.json" % (options.base_url,self.hashedemail)
  172. return "%s/api/person/%s.json" % (options.base_url,self.id)
  173. def isvalid(self):
  174. 'after user data has been loaded in, ensures is valid'
  175. #what to check? email has been validated by get?
  176. return True
  177. def after_load(self):
  178. self.create_user_salt()
  179. self.user_uniqueid = Person.create_userunique()
  180. if self.email != None:
  181. self.set_email(self.email)
  182. self.hashedemail = Person.create_hashed_email(self.email)
  183. if not hasattr(self,'displayname') and self.email != None:
  184. self.displayname = self.email
  185. if hasattr(self,'raw_password'):
  186. self.set_password(self.raw_password)
  187. def create_password(self, size=7):
  188. """
  189. Generates a password and returns it
  190. """
  191. return ''.join([choice(string.letters + string.digits) for i in range(size)])
  192. @classmethod
  193. def create_userunique(self):
  194. """
  195. classmethod, Creates a random set of characters for user unique
  196. "code", use as a guid type thing in querystring's
  197. returns the value of user_unquieid
  198. """
  199. return hashlib.md5(str(random.random())).hexdigest()
  200. @classmethod
  201. def create_random_email(cls,domain='@demisauce.org'):
  202. """
  203. create a random email for testing
  204. accepts a @demisauce.org domain argument optionally
  205. """
  206. return '%s%s' % (hashlib.md5(str(random.random())).hexdigest(),
  207. domain)
  208. @classmethod
  209. def create_hashed_email(self,email):
  210. """
  211. Classmethod to accept an email and hash it into md5 hash
  212. that is a Gravatar_ email
  213. returns hashed email
  214. .. _Gravatar: http://www.gravatar.com/
  215. """
  216. return hashlib.md5(email.lower()).hexdigest()
  217. def set_email(self,email):
  218. self.email = email.lower()
  219. self.hashedemail = hashlib.md5(email.lower()).hexdigest()
  220. def create_user_salt(self):
  221. "creates random salt"
  222. self.random_salt = hashlib.md5(str(random.random())).hexdigest()[:15]
  223. def pwd_encrypt(self,raw_password):
  224. 'encrypt the pwd'
  225. encobj = AES.new(options.demisauce_secret + self.random_salt,, AES.MODE_CFB)
  226. ciphertext = encobj.encrypt(raw_password)
  227. self.password = ciphertext
  228. def pwd_decrypt(self):
  229. encobj = AES.new(options.demisauce_secret + self.random_salt, AES.MODE_CFB)
  230. plaintext = encobj.decrypt(self.password)
  231. return plaintext
  232. def set_password(self, raw_password):
  233. if self.random_salt == None or len(self.random_salt) < 5:
  234. self.create_user_salt()
  235. #TODO: this should be site specific setting
  236. self.pwd_encrypt(raw_password)
  237. self.hashed_password = hashlib.sha1(self.random_salt+raw_password).hexdigest()
  238. def is_authenticated(self, supplied_pwd):
  239. """
  240. Returns a boolean of whether the supplied password was correct.
  241. """
  242. if self.random_salt == None: return False
  243. return (self.hashed_password == hashlib.sha1(self.random_salt+supplied_pwd).hexdigest())
  244. def public_token(self):
  245. """create's a token for user """
  246. return hashlib.md5(self.random_salt+str(self.id)).hexdigest()
  247. def help_tickets(self,ct=10):
  248. """Returns list of help tickets i have submited"""
  249. from demisauce.model.help import Help
  250. return meta.DBSession.query(Help).filter_by(
  251. site_id=self.site_id, hashedemail=self.hashedemail
  252. ).order_by(Help.created.desc()).limit(ct)
  253. def recent_comments(self,ct=5):
  254. """Returns recent comments"""
  255. from demisauce.model.help import Help
  256. return meta.DBSession.query(model.comment.Comment).filter_by(
  257. site_id=self.site_id, hashedemail=self.hashedemail
  258. ).order_by(model.comment.Comment.created.desc()).limit(ct)
  259. def get_recent_activities(self,ct=5):
  260. return self.activities.order_by(Activity.created.desc()).limit(ct)
  261. recent_activities = property(get_recent_activities)
  262. # ===== Serialization
  263. def to_dict_api(self):
  264. output = self.to_dict(keys=self._api_keys)
  265. if self.attributes:
  266. attributes = []
  267. for attr in self.attributes:
  268. attributes.append(attr.to_dict(keys=['name','value','encoding','category','id','object_type','object_id']))
  269. if len(attributes) > 0:
  270. output['attributes'] = attributes
  271. return output
  272. def to_dict_basic(self):
  273. "Basic User info to dict"
  274. return self.to_dict(keys=['id','displayname','email','user_uniqueid'])
  275. def to_json(self,keys=None):
  276. output = self.to_dict(keys=keys)
  277. if hasattr(self,'attributes'):
  278. attributes = []
  279. for attr in self.attributes:
  280. attributes.append(attr.to_dict())
  281. output['attributes'] = attributes
  282. output['session'] = self.session
  283. return escape.json_encode(output)
  284. def from_json(self,json_string):
  285. """
  286. Chainable - Converts from a json string back to a populated object::
  287. peep = Person().from_json(json_string)
  288. """
  289. json_dict = escape.json_decode(json_string)
  290. self.from_dict(json_dict)
  291. if 'attributes' in json_dict:
  292. #self.attributes = []
  293. for attribute in json_dict['attributes']:
  294. self.attributes.append(UserAttribute().from_dict(attribute))
  295. if 'session' in json_dict:
  296. self.session = json_dict['session']
  297. return self
  298. def __str__(self):
  299. return "{person:{site_id:%s, id:%s, email:%s}}" % (self.site_id,self.id,self.email)
  300. def has_role(self,role):
  301. """returns true/false if a user has a specific role
  302. or accepts a list of roles and if user has any of those roles"""
  303. roles = []
  304. if self.isadmin:
  305. roles.append('admin')
  306. if self.issysadmin:
  307. roles.append('sysadmin')
  308. if type(role) == list:
  309. for r in role:
  310. if r in roles:
  311. return True
  312. else:
  313. if role in roles:
  314. return True
  315. return False
  316. def delete(self):
  317. res = meta.engine.execute(text("delete from activity where person_id=%s" % self.id))
  318. res = meta.engine.execute(text("delete from userattribute where person_id=%s" % self.id))
  319. res = meta.engine.execute(text("delete from person where id = %s" % self.id))
  320. def get_attribute(self,name):
  321. """Returns the UserAttribute object for given name"""
  322. for attribute in self.attributes:
  323. if attribute.name == name:
  324. if attribute.encoding == 'json' and isinstance(attribute.value,(str,unicode)):
  325. attribute.value = json.loads(attribute.value)
  326. return attribute
  327. return None
  328. def get_val(self,name):
  329. attr = self.get_attribute(name)
  330. if attr:
  331. return attr.value
  332. return None
  333. def has_attribute(self,name):
  334. """Returns True/False if it has given key attribute"""
  335. for attribute in self.attributes:
  336. if attribute.name == name:
  337. return True
  338. return False
  339. def has_attribute_value(self,name,value):
  340. """Returns True/False if it has given key/value pair attribute"""
  341. for attribute in self.attributes:
  342. if attribute.name == name and value == attribute.value:
  343. return True
  344. return False
  345. def add_attribute(self,name,value,object_type="attribute",category="segment",encoding='str'):
  346. """Add or Update attribute """
  347. attr = self.get_attribute(name)
  348. if isinstance(value,(dict,list)) or encoding == 'json':
  349. value = json.dumps(value)
  350. encoding = 'json'
  351. if attr:
  352. attr.encoding = encoding
  353. attr.value = value
  354. return attr
  355. else:
  356. attr = UserAttribute(object_type=object_type,
  357. name=name,value=value,category=category,encoding=encoding)
  358. if self._is_cache:
  359. attr.person_id = self.id
  360. meta.DBSession().commit(attr)
  361. else:
  362. self.attributes.append(attr)
  363. return attr
  364. return None
  365. @classmethod
  366. def by_site(cls,site_id=0):
  367. """
  368. Gets list of all persons for a site (could be large)
  369. """
  370. return meta.DBSession.query(Person).filter_by(site_id=site_id).all()
  371. @classmethod
  372. def by_unique(self,user_unique=''):
  373. """Get the user by Unique ID"""
  374. return meta.DBSession.query(Person).filter_by(user_uniqueid=user_unique).first()
  375. @classmethod
  376. def by_hashedemail(self,site_id=0,hash=''):
  377. """Get the user by hashed email"""
  378. return meta.DBSession.query(Person).filter_by(site_id=site_id,hashedemail=hash).first()
  379. @classmethod
  380. def by_email(self,site_id=0,email=''):
  381. """Get the user by email"""
  382. return meta.DBSession.query(Person).filter_by(site_id=site_id,email=email.lower()).first()
  383. @classmethod
  384. def by_foreignid(self,site_id=0,id=0):
  385. """Get the user by foreignid"""
  386. return meta.DBSession.query(Person).filter_by(site_id=site_id,foreign_id=id).first()
  387. @classmethod
  388. def by_email(self,site_id=0,email=''):
  389. """Get the user by hashed email"""
  390. return meta.DBSession.query(Person).filter_by(site_id=site_id,email=email).first()
  391. class Group(ModelBase,SerializationMixin):
  392. """Groups of users
  393. :name: name of this group
  394. :email_list: comma delimited list of email address's of this group
  395. :members: List of userattribute objects
  396. :created: date created
  397. :description: description
  398. """
  399. __jsonkeys__ = ['name','members','site_id','created']
  400. _allowed_api_keys = ['name','members','extra_json']
  401. _readonly_keys = ['emails']
  402. schema = group_table
  403. def __init__(self,**kwargs):
  404. super(Group, self).__init__(**kwargs)
  405. self.init_on_load()
  406. def XXfrom_dict(self,json_dict={},allowed_keys=None):
  407. new_dict = json_dict
  408. if 'apikey' in new_dict:
  409. new_dict.pop('apikey')
  410. if 'emails' in new_dict:
  411. new_dict.pop('emails')
  412. #json_dict['extra_json'] = new_dict
  413. super(Group, self).from_dict(json_dict,allowed_keys=allowed_keys)
  414. @orm.reconstructor
  415. def init_on_load(self):
  416. "called by sa after load as init type"
  417. super(Group, self).init_on_load()
  418. def save(self):
  419. if ((not hasattr(self,'slug')) or self.slug == None) and self.name != None:
  420. self.slug = self.makekey(self.name)
  421. super(Group, self).save()
  422. def get_email_list(self):
  423. return ', '.join(['%s' % m.member.email.strip(string.whitespace).lower() for m in self.members])
  424. email_list = property(get_email_list)
  425. def add_memberlist(self,member_list):
  426. """
  427. Accepts a comma delimited list of contacts
  428. returns a tuple of lists, first of new users not already in this group
  429. 2nd of users newly added to system
  430. """
  431. #ml = [m.strip(string.whitespace) for m in member_list.replace(';',',').split(',') if len(m) > 4]
  432. if isinstance(member_list,(list)):
  433. ml_temp = member_list
  434. elif isinstance(member_list,(str,unicode)):
  435. ml_temp = member_list.replace(';',',').lower().split(',')
  436. else:
  437. raise Exception("Memberlist must be list, or comma delimited string")
  438. cl = [ e.strip(string.whitespace) for e in ml_temp if len(e) > 5]
  439. ml = ['%s' % m.member.email.strip(string.whitespace).lower() for m in self.members]
  440. newlist = [e for e in cl if not ml.__contains__(e.strip(string.whitespace))]
  441. removelist = [e for e in ml if not cl.__contains__(e.strip())]
  442. [self.remove_member(e) for e in removelist]
  443. addedusers = [self.add_member(e) for e in newlist]
  444. newtosite = [e for e in addedusers if e != None]
  445. newtogroup = [e for e in newlist if not addedusers.__contains__(e)]
  446. #self.contacts = ','.join(self.__members)
  447. return newtogroup, newtosite
  448. def remove_member(self, newemail):
  449. """
  450. removes a user
  451. """
  452. [self.members.remove(p) for p in self.members if p.email.lower() == newemail]
  453. def member_byemail(self, email):
  454. """
  455. returns a user from members list that has given email?
  456. """
  457. for p in self.members:
  458. if p.email.lower() == email:
  459. return p
  460. return None
  461. def add_member(self, newemail):
  462. # don't trust this user is new
  463. p = Person.by_email(self.site_id,newemail)
  464. if not p:
  465. p = Person(site_id=self.site_id,email=newemail)
  466. else:
  467. newemail = None
  468. self.add_user(p)
  469. return newemail
  470. def add_user(self,user):
  471. ua = UserAttribute(object_type='group',category='group')
  472. user.attributes.append(ua)
  473. self.members.append(ua)
  474. @classmethod
  475. def by_site(cls,site_id=0,ct=15,filter='new'):
  476. """Class method to get groups"""
  477. return meta.DBSession.query(Group).filter_by(
  478. site_id=site_id).order_by(group_table.c.created.desc()) #.limit(ct)
  479. @classmethod
  480. def by_slug(cls,site_id=0,slug=''):
  481. """
  482. Gets the group by slug for a site::
  483. Group.by_slug(c.site_id,'all-users')
  484. """
  485. return meta.DBSession.query(Group).filter_by(site_id=site_id,slug=slug).first()
  486. class UserAttribute(ModelBase,SerializationMixin):
  487. __jsonkeys__ = ['name','value','encoding','category','id','object_type','object_id']
  488. _allowed_api_keys = ['name','value','encoding','category','object_type','object_id']
  489. schema = userattribute_table
  490. class GroupUserAttribute(UserAttribute):
  491. def on_new(self):
  492. self.object_type = 'group'
  493. def userlistable(cls,name="users"):
  494. """User - List Mixin/Mutator"""
  495. mapper = class_mapper(cls)
  496. table = mapper.local_table
  497. cls_name = str(cls)
  498. cls_name = cls_name[cls_name.rfind('.')+1:cls_name.rfind('\'')].lower()
  499. mapper.add_property(name, dynamic_loader(UserAttribute,
  500. primaryjoin=and_(table.c.id==userattribute_table.c.object_id,userattribute_table.c.object_type==cls_name),
  501. foreign_keys=[userattribute_table.c.object_id],
  502. backref='%s' % table.name))
  503. #log.debug("userlistable table.name = %s" % table.name)
  504. # initialize some stuff
  505. def on_new(self):
  506. self.object_type = cls_name
  507. setattr(cls, "on_new", on_new)