4 from datetime
import datetime
6 from itertools
import imap
10 from couchdbkit
.ext
.django
.schema
import *
11 from uuidfield
import UUIDField
13 from django
.core
.exceptions
import ValidationError
14 from django
.core
.validators
import RegexValidator
15 from django
.db
import transaction
, models
16 from django
.db
.models
import Q
17 from django
.contrib
.auth
.models
import User
as DjangoUser
18 from django
.contrib
.auth
import get_user_model
19 from django
.utils
.translation
import ugettext_lazy
as _
20 from django
.conf
import settings
21 from django
.core
.cache
import cache
22 from django
.contrib
import messages
24 from mygpo
.core
.models
import (TwitterModel
, UUIDModel
, SettingsModel
,
25 GenericManager
, DeleteableModel
, )
26 from mygpo
.podcasts
.models
import Podcast
, Episode
27 from mygpo
.utils
import random_token
28 from mygpo
.core
.proxy
import DocumentABCMeta
, proxy_object
29 from mygpo
.decorators
import repeat_on_conflict
30 from mygpo
.users
.ratings
import RatingMixin
31 from mygpo
.users
.subscriptions
import (subscription_changes
,
32 podcasts_for_states
, get_subscribed_podcast_ids
)
33 from mygpo
.users
.settings
import FAV_FLAG
, PUBLIC_SUB_PODCAST
, SettingsMixin
34 from mygpo
.db
.couchdb
.user
import user_history
, device_history
36 # make sure this code is executed at startup
37 from mygpo
.users
.signals
import *
40 logger
= logging
.getLogger(__name__
)
43 RE_DEVICE_UID
= re
.compile(r
'^[\w.-]+$')
45 # TODO: derive from ValidationException?
46 class InvalidEpisodeActionAttributes(ValueError):
47 """ raised when the attribues of an episode action fail validation """
50 class SubscriptionException(Exception):
51 """ raised when a subscription can not be modified """
54 class DeviceDoesNotExist(Exception):
58 class DeviceDeletedException(DeviceDoesNotExist
):
62 GroupedDevices
= collections
.namedtuple('GroupedDevices', 'is_synced devices')
65 class UIDValidator(RegexValidator
):
66 """ Validates that the Device UID conforms to the given regex """
68 message
= 'Invalid Device ID'
72 class UserProxyQuerySet(models
.QuerySet
):
74 def by_username_or_email(self
, username
, email
):
75 """ Queries for a User by username or email """
79 q |
= Q(username
=username
)
90 class UserProxyManager(GenericManager
):
91 """ Manager for the UserProxy model """
93 def get_queryset(self
):
94 return UserProxyQuerySet(self
.model
, using
=self
._db
)
96 def from_user(self
, user
):
97 """ Get the UserProxy corresponding for the given User """
98 return self
.get(pk
=user
.pk
)
101 class UserProxy(DjangoUser
):
103 objects
= UserProxyManager()
110 self
.is_active
= True
113 self
.profile
.activation_key
= None
117 def get_grouped_devices(self
):
118 """ Returns groups of synced devices and a unsynced group """
120 clients
= Client
.objects
.filter(user
=self
, deleted
=False)\
121 .order_by('-sync_group')
123 last_group
= object()
126 for client
in clients
:
127 # check if we have just found a new group
128 if last_group
!= client
.sync_group
:
132 group
= GroupedDevices(client
.sync_group
is not None, [])
134 last_group
= client
.sync_group
135 group
.devices
.append(client
)
137 # yield remaining group
142 class UserProfile(TwitterModel
, SettingsModel
):
143 """ Additional information stored for a User """
145 # the user to which this profile belongs
146 user
= models
.OneToOneField(settings
.AUTH_USER_MODEL
,
147 related_name
='profile')
149 # the CouchDB _id of the user
150 uuid
= UUIDField(unique
=True)
152 # if False, suggestions should be updated
153 suggestions_up_to_date
= models
.BooleanField(default
=False)
155 # text the user entered about himeself
156 about
= models
.TextField(blank
=True)
158 # Google email address for OAuth login
159 google_email
= models
.CharField(max_length
=100, null
=True)
161 # token for accessing subscriptions of this use
162 subscriptions_token
= models
.CharField(max_length
=32, null
=True,
163 default
=random_token
)
165 # token for accessing the favorite-episodes feed of this user
166 favorite_feeds_token
= models
.CharField(max_length
=32, null
=True,
167 default
=random_token
)
169 # token for automatically updating feeds published by this user
170 publisher_update_token
= models
.CharField(max_length
=32, null
=True,
171 default
=random_token
)
173 # token for accessing the userpage of this user
174 userpage_token
= models
.CharField(max_length
=32, null
=True,
175 default
=random_token
)
177 # key for activating the user
178 activation_key
= models
.CharField(max_length
=40, null
=True)
180 def get_token(self
, token_name
):
181 """ returns a token """
183 if token_name
not in TOKEN_NAMES
:
184 raise TokenException('Invalid token name %s' % token_name
)
186 return getattr(self
, token_name
)
189 class Suggestions(Document
, RatingMixin
):
190 user
= StringProperty(required
=True)
191 user_oldid
= IntegerProperty()
192 podcasts
= StringListProperty()
193 blacklist
= StringListProperty()
196 def get_podcasts(self
, count
=None):
197 User
= get_user_model()
198 user
= User
.objects
.get(profile__uuid
=self
.user
)
199 # TODO: re-include later on
200 #subscriptions = get_subscribed_podcast_ids(user)
203 ids
= filter(lambda x
: not x
in self
.blacklist
+ subscriptions
, self
.podcasts
)
207 podcasts
= Podcast
.objects
.filter(id__in
=ids
).prefetch_related('slugs')
208 return filter(lambda x
: x
and x
.title
, podcasts
)
213 return super(Suggestions
, self
).__repr
__()
215 return '%d Suggestions for %s (%s)' % \
216 (len(self
.podcasts
), self
.user
, self
._id
)
219 class EpisodeAction(DocumentSchema
):
221 One specific action to an episode. Must
222 always be part of a EpisodeUserState
225 action
= StringProperty(required
=True)
227 # walltime of the event (assigned by the uploading client, defaults to now)
228 timestamp
= DateTimeProperty(required
=True, default
=datetime
.utcnow
)
230 # upload time of the event
231 upload_timestamp
= IntegerProperty(required
=True)
233 device_oldid
= IntegerProperty(required
=False)
234 device
= StringProperty()
235 started
= IntegerProperty()
236 playmark
= IntegerProperty()
237 total
= IntegerProperty()
239 def __eq__(self
, other
):
240 if not isinstance(other
, EpisodeAction
):
242 vals
= ('action', 'timestamp', 'device', 'started', 'playmark',
244 return all([getattr(self
, v
, None) == getattr(other
, v
, None) for v
in vals
])
247 def to_history_entry(self
):
248 entry
= HistoryEntry()
249 entry
.action
= self
.action
250 entry
.timestamp
= self
.timestamp
251 entry
.device_id
= self
.device
252 entry
.started
= self
.started
253 entry
.position
= self
.playmark
254 entry
.total
= self
.total
259 def validate_time_values(self
):
260 """ Validates allowed combinations of time-values """
262 PLAY_ACTION_KEYS
= ('playmark', 'started', 'total')
264 # Key found, but must not be supplied (no play action!)
265 if self
.action
!= 'play':
266 for key
in PLAY_ACTION_KEYS
:
267 if getattr(self
, key
, None) is not None:
268 raise InvalidEpisodeActionAttributes('%s only allowed in play actions' % key
)
270 # Sanity check: If started or total are given, require playmark
271 if ((self
.started
is not None) or (self
.total
is not None)) and \
272 self
.playmark
is None:
273 raise InvalidEpisodeActionAttributes('started and total require position')
275 # Sanity check: total and playmark can only appear together
276 if ((self
.total
is not None) or (self
.started
is not None)) and \
277 ((self
.total
is None) or (self
.started
is None)):
278 raise InvalidEpisodeActionAttributes('total and started can only appear together')
282 return '%s-Action on %s at %s (in %s)' % \
283 (self
.action
, self
.device
, self
.timestamp
, self
._id
)
287 return hash(frozenset([self
.action
, self
.timestamp
, self
.device
,
288 self
.started
, self
.playmark
, self
.total
]))
291 class Chapter(Document
):
292 """ A user-entered episode chapter """
294 device
= StringProperty()
295 created
= DateTimeProperty()
296 start
= IntegerProperty(required
=True)
297 end
= IntegerProperty(required
=True)
298 label
= StringProperty()
299 advertisement
= BooleanProperty()
303 return '<%s %s (%d-%d)>' % (self
.__class
__.__name
__, self
.label
,
304 self
.start
, self
.end
)
307 class EpisodeUserState(Document
, SettingsMixin
):
309 Contains everything a user has done with an Episode
312 episode
= StringProperty(required
=True)
313 actions
= SchemaListProperty(EpisodeAction
)
314 user_oldid
= IntegerProperty()
315 user
= StringProperty(required
=True)
316 ref_url
= StringProperty(required
=True)
317 podcast_ref_url
= StringProperty(required
=True)
318 merged_ids
= StringListProperty()
319 chapters
= SchemaListProperty(Chapter
)
320 podcast
= StringProperty(required
=True)
324 def add_actions(self
, actions
):
325 map(EpisodeAction
.validate_time_values
, actions
)
326 self
.actions
= list(self
.actions
) + actions
327 self
.actions
= list(set(self
.actions
))
328 self
.actions
= sorted(self
.actions
, key
=lambda x
: x
.timestamp
)
331 def is_favorite(self
):
332 return self
.get_wksetting(FAV_FLAG
)
335 def set_favorite(self
, set_to
=True):
336 self
.settings
[FAV_FLAG
.name
] = set_to
339 def get_history_entries(self
):
340 return imap(EpisodeAction
.to_history_entry
, self
.actions
)
344 return 'Episode-State %s (in %s)' % \
345 (self
.episode
, self
._id
)
347 def __eq__(self
, other
):
348 if not isinstance(other
, EpisodeUserState
):
351 return (self
.episode
== other
.episode
and
352 self
.user
== other
.user
)
356 class SubscriptionAction(Document
):
357 action
= StringProperty()
358 timestamp
= DateTimeProperty(default
=datetime
.utcnow
)
359 device
= StringProperty()
362 __metaclass__
= DocumentABCMeta
365 def __cmp__(self
, other
):
366 return cmp(self
.timestamp
, other
.timestamp
)
368 def __eq__(self
, other
):
369 return self
.action
== other
.action
and \
370 self
.timestamp
== other
.timestamp
and \
371 self
.device
== other
.device
374 return hash(self
.action
) + hash(self
.timestamp
) + hash(self
.device
)
377 return '<SubscriptionAction %s on %s at %s>' % (
378 self
.action
, self
.device
, self
.timestamp
)
381 class PodcastUserState(Document
, SettingsMixin
):
383 Contains everything that a user has done
384 with a specific podcast and all its episodes
387 podcast
= StringProperty(required
=True)
388 user_oldid
= IntegerProperty()
389 user
= StringProperty(required
=True)
390 actions
= SchemaListProperty(SubscriptionAction
)
391 tags
= StringListProperty()
392 ref_url
= StringProperty(required
=True)
393 disabled_devices
= StringListProperty()
394 merged_ids
= StringListProperty()
397 def remove_device(self
, device
):
399 Removes all actions from the podcast state that refer to the
402 self
.actions
= filter(lambda a
: a
.device
!= device
.id, self
.actions
)
405 def subscribe(self
, device
):
406 action
= SubscriptionAction()
407 action
.action
= 'subscribe'
408 action
.device
= device
.id.hex
409 self
.add_actions([action
])
412 def unsubscribe(self
, device
):
413 action
= SubscriptionAction()
414 action
.action
= 'unsubscribe'
415 action
.device
= device
.id.hex
416 self
.add_actions([action
])
419 def add_actions(self
, actions
):
420 self
.actions
= list(set(self
.actions
+ actions
))
421 self
.actions
= sorted(self
.actions
)
424 def add_tags(self
, tags
):
425 self
.tags
= list(set(self
.tags
+ tags
))
428 def set_device_state(self
, devices
):
429 disabled_devices
= [device
.id for device
in devices
if device
.deleted
]
430 #self.disabled_devices = disabled_devices
433 def get_change_between(self
, device_id
, since
, until
):
435 Returns the change of the subscription status for the given device
436 between the two timestamps.
438 The change is given as either 'subscribe' (the podcast has been
439 subscribed), 'unsubscribed' (the podcast has been unsubscribed) or
443 device_actions
= filter(lambda x
: x
.device
== device_id
, self
.actions
)
444 before
= filter(lambda x
: x
.timestamp
<= since
, device_actions
)
445 after
= filter(lambda x
: x
.timestamp
<= until
, device_actions
)
447 # nothing happened, so there can be no change
451 then
= before
[-1] if before
else None
455 if now
.action
!= 'unsubscribe':
457 elif then
.action
!= now
.action
:
462 def get_subscribed_device_ids(self
):
463 """ device Ids on which the user subscribed to the podcast """
466 for action
in self
.actions
:
467 if action
.action
== "subscribe":
468 if not action
.device
in self
.disabled_devices
:
469 devices
.add(action
.device
)
471 if action
.device
in devices
:
472 devices
.remove(action
.device
)
477 def is_subscribed_on(self
, device
):
478 """ checks if the podcast is subscribed on the given device """
480 for action
in reversed(self
.actions
):
481 if not action
.device
== device
.id.hex:
484 # we only need to check the latest action for the device
485 return (action
.action
== 'subscribe')
487 # we haven't found any matching action
492 return self
.get_wksetting(PUBLIC_SUB_PODCAST
)
495 def __eq__(self
, other
):
499 return self
.podcast
== other
.podcast
and \
500 self
.user
== other
.user
503 return 'Podcast %s for User %s (%s)' % \
504 (self
.podcast
, self
.user
, self
._id
)
507 class SyncGroup(models
.Model
):
508 """ A group of Clients """
510 user
= models
.ForeignKey(settings
.AUTH_USER_MODEL
,
511 on_delete
=models
.CASCADE
)
514 """ Sync the group, ie bring all members up-to-date """
516 group_state
= self
.get_group_state()
518 for device
in SyncGroup
.objects
.filter(sync_group
=self
):
519 sync_actions
= self
.get_sync_actions(device
, group_state
)
520 device
.apply_sync_actions(sync_actions
)
522 def get_group_state(self
):
523 """ Returns the group's subscription state
525 The state is represented by the latest actions for each podcast """
527 devices
= Client
.objects
.filter(sync_group
=self
)
531 actions
= dict(d
.get_latest_changes())
532 for podcast_id
, action
in actions
.items():
533 if not podcast_id
in state
or \
534 action
.timestamp
> state
[podcast_id
].timestamp
:
535 state
[podcast_id
] = action
539 def get_sync_actions(self
, device
, group_state
):
540 """ Get the actions required to bring the device to the group's state
542 After applying the actions the device reflects the group's state """
544 # Filter those that describe actual changes to the current state
546 current_state
= dict(device
.get_latest_changes())
548 for podcast_id
, action
in group_state
.items():
550 # Sync-Actions must be newer than current state
551 if podcast_id
in current_state
and \
552 action
.timestamp
<= current_state
[podcast_id
].timestamp
:
555 # subscribe only what hasn't been subscribed before
556 if action
.action
== 'subscribe' and \
557 (podcast_id
not in current_state
or \
558 current_state
[podcast_id
].action
== 'unsubscribe'):
559 add
.append(podcast_id
)
561 # unsubscribe only what has been subscribed before
562 elif action
.action
== 'unsubscribe' and \
563 podcast_id
in current_state
and \
564 current_state
[podcast_id
].action
== 'subscribe':
565 rem
.append(podcast_id
)
571 class Client(UUIDModel
, DeleteableModel
):
572 """ A client application """
582 (DESKTOP
, _('Desktop')),
583 (LAPTOP
, _('Laptop')),
584 (MOBILE
, _('Cell phone')),
585 (SERVER
, _('Server')),
586 (TABLET
, _('Tablet')),
590 # User-assigned ID; must be unique for the user
591 uid
= models
.CharField(max_length
=64, validators
=[UIDValidator()])
593 # the user to which the Client belongs
594 user
= models
.ForeignKey(settings
.AUTH_USER_MODEL
,
595 on_delete
=models
.CASCADE
)
598 name
= models
.CharField(max_length
=100, default
='New Device')
600 # one of several predefined types
601 type = models
.CharField(max_length
=max(len(k
) for k
, v
in TYPES
),
602 choices
=TYPES
, default
=OTHER
)
604 # user-agent string from which the Client was last accessed (for writing)
605 user_agent
= models
.CharField(max_length
=300, null
=True, blank
=True)
607 sync_group
= models
.ForeignKey(SyncGroup
, null
=True,
608 on_delete
=models
.PROTECT
)
616 def sync_with(self
, other
):
617 """ Puts two devices in a common sync group"""
619 if self
.user
!= other
.user
:
620 raise ValueError('the devices do not belong to the user')
622 if self
.sync_group
is not None and \
623 other
.sync_group
is not None and \
624 self
.sync_group
!= other
.sync_group
:
626 ogroup
= other
.sync_group
627 Client
.objects
.filter(sync_group
=ogroup
)\
628 .update(sync_group
=self
.sync_group
)
631 elif self
.sync_group
is None and \
632 other
.sync_group
is None:
633 sg
= SyncGroup
.objects
.create(user
=self
.user
)
634 other
.sync_group
= sg
639 elif self
.sync_group
is not None:
640 self
.sync_group
= other
.sync_group
643 elif other
.sync_group
is not None:
644 other
.sync_group
= self
.sync_group
648 """ Stop synchronisation with other clients """
651 logger
.info('Stopping synchronisation of %r', self
)
652 self
.sync_group
= None
655 clients
= Client
.objects
.filter(sync_group
=sg
)
656 logger
.info('%d other clients remaining in sync group', len(clients
))
659 logger
.info('Deleting sync group %r', sg
)
660 for client
in clients
:
661 client
.sync_group
= None
666 def get_sync_targets(self
):
667 """ Returns the devices and groups with which the device can be synced
669 Groups are represented as lists of devices """
673 user
= UserProxy
.objects
.from_user(self
.user
)
674 for group
in user
.get_grouped_devices():
676 if self
in group
.devices
:
677 # the device's group can't be a sync-target
680 elif group
.is_synced
:
684 # every unsynced device is a sync-target
685 for dev
in group
.devices
:
689 def apply_sync_actions(self
, sync_actions
):
690 """ Applies the sync-actions to the client """
692 from mygpo
.db
.couchdb
.podcast_state
import subscribe
, unsubscribe
693 from mygpo
.users
.models
import SubscriptionException
694 add
, rem
= sync_actions
696 podcasts
= Podcast
.objects
.filter(id__in
=(add
+rem
))
697 podcasts
= {podcast
.id: podcast
for podcast
in podcasts
}
699 for podcast_id
in add
:
700 podcast
= podcasts
.get(podcast_id
, None)
704 subscribe(podcast
, self
.user
, self
)
705 except SubscriptionException
as e
:
706 logger
.warn('Web: %(username)s: cannot sync device: %(error)s' %
707 dict(username
=self
.user
.username
, error
=repr(e
)))
709 for podcast_id
in rem
:
710 podcast
= podcasts
.get(podcast_id
, None)
715 unsubscribe(podcast
, self
.user
, self
)
716 except SubscriptionException
as e
:
717 logger
.warn('Web: %(username)s: cannot sync device: %(error)s' %
718 dict(username
=self
.user
.username
, error
=repr(e
)))
720 def get_subscription_changes(self
, since
, until
):
722 Returns the subscription changes for the device as two lists.
723 The first lists contains the Ids of the podcasts that have been
724 subscribed to, the second list of those that have been unsubscribed
728 from mygpo
.db
.couchdb
.podcast_state
import podcast_states_for_device
729 podcast_states
= podcast_states_for_device(self
.id.hex)
730 return subscription_changes(self
.id.hex, podcast_states
, since
, until
)
732 def get_latest_changes(self
):
733 from mygpo
.db
.couchdb
.podcast_state
import podcast_states_for_device
734 podcast_states
= podcast_states_for_device(self
.id.hex)
735 for p_state
in podcast_states
:
736 actions
= filter(lambda x
: x
.device
== self
.id.hex, reversed(p_state
.actions
))
738 yield (p_state
.podcast
, actions
[0])
740 def get_subscribed_podcast_ids(self
):
741 from mygpo
.db
.couchdb
.podcast_state
import get_subscribed_podcast_states_by_device
742 states
= get_subscribed_podcast_states_by_device(self
)
743 return [state
.podcast
for state
in states
]
745 def get_subscribed_podcasts(self
):
746 """ Returns all subscribed podcasts for the device
748 The attribute "url" contains the URL that was used when subscribing to
750 from mygpo
.db
.couchdb
.podcast_state
import get_subscribed_podcast_states_by_device
751 states
= get_subscribed_podcast_states_by_device(self
)
752 return podcasts_for_states(states
)
754 def synced_with(self
):
755 if not self
.sync_group
:
758 return Client
.objects
.filter(sync_group
=self
.sync_group
)\
762 return '{} ({})'.format(self
.name
.encode('ascii', errors
='replace'),
763 self
.uid
.encode('ascii', errors
='replace'))
765 def __unicode__(self
):
766 return u
'{} ({})'.format(self
.name
, self
.uid
)
769 class Device(Document
, SettingsMixin
):
770 id = StringProperty(default
=lambda: uuid
.uuid4().hex)
771 oldid
= IntegerProperty(required
=False)
772 uid
= StringProperty(required
=True)
773 name
= StringProperty(required
=True, default
='New Device')
774 type = StringProperty(required
=True, default
='other')
775 deleted
= BooleanProperty(default
=False)
776 user_agent
= StringProperty()
781 return hash(frozenset([self
.id, self
.uid
, self
.name
, self
.type, self
.deleted
]))
784 def __eq__(self
, other
):
785 return self
.id == other
.id
789 return '<{cls} {id}>'.format(cls
=self
.__class
__.__name
__, id=self
.id)
793 TOKEN_NAMES
= ('subscriptions_token', 'favorite_feeds_token',
794 'publisher_update_token', 'userpage_token')
797 class TokenException(Exception):
801 class History(object):
803 def __init__(self
, user
, device
):
808 def __getitem__(self
, key
):
810 if isinstance(key
, slice):
811 start
= key
.start
or 0
812 length
= key
.stop
- start
818 return device_history(self
.user
, self
.device
, start
, length
)
821 return user_history(self
.user
, start
, length
)
825 class HistoryEntry(object):
826 """ A class that can represent subscription and episode actions """
830 def from_action_dict(cls
, action
):
832 entry
= HistoryEntry()
834 if 'timestamp' in action
:
835 ts
= action
.pop('timestamp')
836 entry
.timestamp
= dateutil
.parser
.parse(ts
)
838 for key
, value
in action
.items():
839 setattr(entry
, key
, value
)
846 return getattr(self
, 'position', None)
850 def fetch_data(cls
, user
, entries
,
851 podcasts
=None, episodes
=None):
852 """ Efficiently loads additional data for a number of entries """
856 podcast_ids
= [getattr(x
, 'podcast_id', None) for x
in entries
]
857 podcast_ids
= filter(None, podcast_ids
)
858 podcasts
= Podcast
.objects
.filter(id__in
=podcast_ids
)\
859 .prefetch_related('slugs')
860 podcasts
= {podcast
.id.hex: podcast
for podcast
in podcasts
}
864 episode_ids
= [getattr(x
, 'episode_id', None) for x
in entries
]
865 episode_ids
= filter(None, episode_ids
)
866 episodes
= Episode
.objects
.filter(id__in
=episode_ids
)\
867 .select_related('podcast')\
868 .prefetch_related('slugs',
870 episodes
= {episode
.id.hex: episode
for episode
in episodes
}
873 # does not need pre-populated data because no db-access is required
874 device_ids
= [getattr(x
, 'device_id', None) for x
in entries
]
875 device_ids
= filter(None, device_ids
)
876 devices
= {client
.id.hex: client
for client
in user
.client_set
.all()}
879 for entry
in entries
:
880 podcast_id
= getattr(entry
, 'podcast_id', None)
881 entry
.podcast
= podcasts
.get(podcast_id
, None)
883 episode_id
= getattr(entry
, 'episode_id', None)
884 entry
.episode
= episodes
.get(episode_id
, None)
886 if hasattr(entry
, 'user'):
889 device
= devices
.get(getattr(entry
, 'device_id', None), None)
890 entry
.device
= device
896 def create_missing_profile(sender
, **kwargs
):
897 """ Creates a UserProfile if a User doesn't have one """
898 user
= kwargs
['instance']
900 if not hasattr(user
, 'profile'):
901 # TODO: remove uuid column once migration from CouchDB is complete
903 profile
= UserProfile
.objects
.create(user
=user
, uuid
=uuid
.uuid1())
904 user
.profile
= profile