def test_deconstruct_kwargs_kept(self): instance = TaggableManager(through=OfficialThroughModel, to="dummy.To") name, path, args, kwargs = instance.deconstruct() new_instance = TaggableManager(*args, **kwargs) self.assertEqual( "tests.OfficialThroughModel", new_instance.remote_field.through ) self.assertEqual("dummy.To", new_instance.remote_field.model)
def test_formfield(self): tm = TaggableManager(verbose_name='categories', help_text='Add some categories', blank=True) ff = tm.formfield() self.assertEqual(ff.label, 'categories') self.assertEqual(ff.help_text, u'Add some categories') self.assertEqual(ff.required, False) self.assertEqual(ff.clean(""), []) tm = TaggableManager() ff = tm.formfield() self.assertRaises(ValidationError, ff.clean, "")
def test_formfield(self): tm = TaggableManager(verbose_name='categories', help_text='Add some categories', blank=True) ff = tm.formfield() self.assertEqual(ff.label, 'Categories') self.assertEqual(ff.help_text, 'Add some categories') self.assertEqual(ff.required, False) self.assertEqual(list(ff.queryset), list(self.food_model.tags.all())) self.assertEqual(list(ff.clean("")), []) tm = TaggableManager() ff = tm.formfield() self.assertRaises(ValidationError, ff.clean, "")
class Show(models.Model): """ A podcast show, which has many episodes. """ EXPLICIT_CHOICES = ( (1, _("yes")), (2, _("no")), (3, _("clean")), ) uuid = UUIDField(_("id"), unique=True) created = models.DateTimeField(_("created"), auto_now_add=True, editable=False) updated = models.DateTimeField(_("updated"), auto_now=True, editable=False) published = models.DateTimeField(_("published"), null=True, blank=True, editable=False) sites = models.ManyToManyField(Site, verbose_name=_('Sites')) ttl = models.PositiveIntegerField( _("ttl"), default=1440, help_text=_("""``Time to Live,`` the number of minutes a channel can be cached before refreshing.""")) owner = models.ForeignKey( settings.AUTH_USER_MODEL, related_name="podcast_shows", verbose_name=_("owner"), on_delete=models.PROTECT, help_text=_( """Make certain the user account has a name and e-mail address.""") ) editor_email = models.EmailField( _("editor email"), blank=True, help_text=_( "Email address of the person responsible for the feed's content.")) webmaster_email = models.EmailField( _("webmaster email"), blank=True, help_text=_( "Email address of the person responsible for channel publishing.")) if 'licenses' in settings.INSTALLED_APPS: license = models.ForeignKey(License, verbose_name=_("license")) else: license = models.CharField( _("license"), max_length=255, help_text= _("To publish a podcast to iTunes it is required to set a license type." )) organization = models.CharField( _("organization"), max_length=255, help_text= _("Name of the organization, company or Web site producing the podcast." )) link = models.URLField(_("link"), help_text=_("""URL of either the main website or the podcast section of the main website.""")) enable_comments = models.BooleanField(default=True) author_text = models.CharField(_("author text"), max_length=255, help_text=_(""" This tag contains the name of the person or company that is most widely attributed to publishing the Podcast and will be displayed immediately underneath the title of the Podcast. The suggested format is: '[email protected] (Full Name)' but 'Full Name' only, is acceptable. Multiple authors should be comma separated.""")) title = models.CharField(_("title"), max_length=255) slug = AutoSlugField(_("slug"), populate_from="title", unique="True") subtitle = models.CharField( _("subtitle"), max_length=255, help_text=_("Looks best if only a few words, like a tagline.")) # If the show is not on iTunes, many fields may be ignored in your user forms on_itunes = models.BooleanField( _("iTunes"), default=True, help_text=_("Checked if the podcast is submitted to iTunes")) description_pretty = models.TextField( _("pretty description"), blank=True, help_text= "May be longer than 4000 characters and contain HTML tags and styling." ) description = models.TextField(_("description"), max_length=4000, help_text=_(""" This is your chance to tell potential subscribers all about your podcast. Describe your subject matter, media format, episode schedule, and other relevant info so that they know what they'll be getting when they subscribe. In addition, make a list of the most relevant search terms that you want yourp podcast to match, then build them into your description. Note that iTunes removes podcasts that include lists of irrelevant words in the itunes:summary, description, or itunes:keywords tags. This field can be up to 4000 characters.""")) if 'photologue' in settings.INSTALLED_APPS: original_image = models.ForeignKey(Photo, verbose_name=_("image"), default=None, null=True, blank=True, on_delete=models.SET_NULL, help_text=_(""" A podcast must have 1400 x 1400 pixel cover art in JPG or PNG format using RGB color space. See our technical spec for details. To be eligible for featuring on iTunes Stores, choose an attractive, original, and square JPEG (.jpg) or PNG (.png) image at a size of 1400x1400 pixels. The image will be scaled down to 50x50 pixels at smallest in iTunes. For reference see the <a href="http://www.apple.com/itunes/podcasts/specs.html#metadata">iTunes Podcast specs</a>.<br /><br /> For episode artwork to display in iTunes, image must be <a href="http://answers.yahoo.com/question/index?qid=20080501164348AAjvBvQ"> saved to file's <strong>metadata</strong></a> before enclosure uploading!""")) else: original_image = ImageField(_("image"), upload_to=get_show_upload_folder, blank=True, help_text=_(""" A podcast must have 1400 x 1400 pixel cover art in JPG or PNG format using RGB color space. See our technical spec for details. To be eligible for featuring on iTunes Stores, choose an attractive, original, and square JPEG (.jpg) or PNG (.png) image at a size of 1400x1400 pixels. The image will be scaled down to 50x50 pixels at smallest in iTunes. For reference see the <a href="http://www.apple.com/itunes/podcasts/specs.html#metadata">iTunes Podcast specs</a>.<br /><br /> For episode artwork to display in iTunes, image must be <a href="http://answers.yahoo.com/question/index?qid=20080501164348AAjvBvQ"> saved to file's <strong>metadata</strong></a> before enclosure uploading!""")) if ResizeToFill: admin_thumb_sm = ImageSpecField(source="original_image", processors=[ResizeToFill(50, 50)], options={"quality": 100}) admin_thumb_lg = ImageSpecField(source="original_image", processors=[ResizeToFill(450, 450)], options={"quality": 100}) img_show_sm = ImageSpecField(source="original_image", processors=[ResizeToFill(120, 120)], options={"quality": 100}) img_show_lg = ImageSpecField(source="original_image", processors=[ResizeToFill(550, 550)], options={"quality": 100}) img_itunes_sm = ImageSpecField(source="original_image", processors=[ResizeToFill(144, 144)], options={"quality": 100}) img_itunes_lg = ImageSpecField(source="original_image", processors=[ResizeToFill(1400, 1400)], options={"quality": 100}) feedburner = models.URLField( _("feedburner url"), blank=True, help_text=_("""Fill this out after saving this show and at least one episode. URL should look like "http://feeds.feedburner.com/TitleOfShow". See <a href="http://code.google.com/p/django-podcast/">documentation</a> for more. <a href="http://www.feedburner.com/fb/a/ping">Manually ping</a>""" )) # iTunes specific fields explicit = models.PositiveSmallIntegerField( _("explicit"), default=1, choices=EXPLICIT_CHOICES, help_text=_("``Clean`` will put the clean iTunes graphic by it.")) redirect = models.URLField( _("redirect"), blank=True, help_text=_("""The show's new URL feed if changing the URL of the current show feed. Must continue old feed for at least two weeks and write a 301 redirect for old feed.""")) keywords = models.CharField( _("keywords"), max_length=255, blank=True, help_text=_("""A comma-demlimitedlist of up to 12 words for iTunes searches. Perhaps include misspellings of the title.""")) itunes = models.URLField( _("itunes store url"), blank=True, help_text=_("""Fill this out after saving this show and at least one episode. URL should look like: "http://phobos.apple.com/WebObjects/MZStore.woa/wa/viewPodcast?id=000000000". See <a href="http://code.google.com/p/django-podcast/">documentation</a> for more.""" )) twitter_tweet_prefix = models.CharField( _("Twitter tweet prefix"), max_length=80, help_text=_( "Enter a short ``tweet_text`` prefix for new episodes on this show." ), blank=True) objects = ShowQuerySet.as_manager() tags = TaggableManager(blank=True) class Meta: verbose_name = _("Show") verbose_name_plural = _("Shows") ordering = ("organization", "slug") def __str__(self): return self.title def get_share_url(self): return "http://{0}{1}".format(Site.objects.get_current(), self.get_absolute_url()) def get_absolute_url(self): return reverse("podcasting_show_detail", kwargs={"slug": self.slug}) @property def current_episode(self): try: return self.episode_set.published().order_by("-published")[0] except IndexError: return None
class Batch(models.Model): '''A batch has one or more images for some number of patients, each of which is associated with a Study or Session. A batch maps cleanly to a folder that is dropped into data for processing, and the application moves through tasks based on batches. ''' uid = models.CharField(max_length=200, null=False, unique=True) status = models.CharField(choices=BATCH_STATUS, default="NEW", max_length=250) add_date = models.DateTimeField('date added', auto_now_add=True) has_error = models.BooleanField(choices=ERROR_STATUS, default=False, verbose_name="HasError") qa = JSONField(default=dict()) logs = JSONField(default=dict()) modify_date = models.DateTimeField('date modified', auto_now=True) tags = TaggableManager() def change_images_status(self, status): '''change all images to have the same status''' for dcm in self.image_set.all(): dcm.status = status dcm.save() def get_image_paths(self): '''return file paths for all images associated with a batch''' image_files = [] for dcm in self.image_set.all(): try: if hasattr(dcm.image, 'file'): dicom_file = dcm.image.path if os.path.exists(dicom_file): image_files.append(dicom_file) # Image object has no file associated with it except ValueError: pass return image_files def get_finished(self): '''return file paths that aren't in PHI folder''' return [x for x in self.get_image_paths() if "/PHI/" not in x] def get_path(self): return "%s/%s" % (MEDIA_ROOT, self.id) def get_absolute_url(self): return reverse('batch_details', args=[str(self.id)]) def __str__(self): return "%s-%s" % (self.id, self.uid) def __unicode__(self): return "%s-%s" % (self.id, self.uid) def get_label(self): return "batch" class Meta: app_label = 'main'
class FOIARequest(models.Model): """A Freedom of Information Act request""" # pylint: disable=too-many-public-methods # pylint: disable=too-many-instance-attributes user = models.ForeignKey(User) title = models.CharField(max_length=255, db_index=True) slug = models.SlugField(max_length=255) status = models.CharField(max_length=10, choices=STATUS, db_index=True) jurisdiction = models.ForeignKey('jurisdiction.Jurisdiction') agency = models.ForeignKey('agency.Agency', blank=True, null=True) date_submitted = models.DateField(blank=True, null=True, db_index=True) date_updated = models.DateField(blank=True, null=True, db_index=True) date_done = models.DateField(blank=True, null=True, verbose_name='Date response received') date_due = models.DateField(blank=True, null=True, db_index=True) days_until_due = models.IntegerField(blank=True, null=True) date_followup = models.DateField(blank=True, null=True) date_estimate = models.DateField(blank=True, null=True, verbose_name='Estimated Date Completed') date_processing = models.DateField(blank=True, null=True) embargo = models.BooleanField(default=False) permanent_embargo = models.BooleanField(default=False) date_embargo = models.DateField(blank=True, null=True) price = models.DecimalField(max_digits=14, decimal_places=2, default='0.00') requested_docs = models.TextField(blank=True) description = models.TextField(blank=True) featured = models.BooleanField(default=False) tracker = models.BooleanField(default=False) sidebar_html = models.TextField(blank=True) tracking_id = models.CharField(blank=True, max_length=255) mail_id = models.CharField(blank=True, max_length=255, editable=False) updated = models.BooleanField(default=False) email = models.CharField(blank=True, max_length=254) other_emails = fields.EmailsListField(blank=True, max_length=255) times_viewed = models.IntegerField(default=0) disable_autofollowups = models.BooleanField(default=False) missing_proxy = models.BooleanField( default=False, help_text='This request requires a proxy to file, but no such ' 'proxy was avilable upon draft creation.') parent = models.ForeignKey('self', blank=True, null=True, on_delete=models.SET_NULL) block_incoming = models.BooleanField( default=False, help_text=('Block emails incoming to this request from ' 'automatically being posted on the site')) crowdfund = models.OneToOneField('crowdfund.Crowdfund', related_name='foia', blank=True, null=True) multirequest = models.ForeignKey( 'foia.FOIAMultiRequest', blank=True, null=True, ) read_collaborators = models.ManyToManyField( User, related_name='read_access', blank=True, ) edit_collaborators = models.ManyToManyField( User, related_name='edit_access', blank=True, ) access_key = models.CharField(blank=True, max_length=255) objects = FOIARequestQuerySet.as_manager() tags = TaggableManager(through=TaggedItemBase, blank=True) foia_type = 'foia' def __unicode__(self): return self.title def get_absolute_url(self): """The url for this object""" return reverse('foia-detail', kwargs={ 'jurisdiction': self.jurisdiction.slug, 'jidx': self.jurisdiction.pk, 'slug': self.slug, 'idx': self.pk, }) def save(self, *args, **kwargs): """Normalize fields before saving and set the embargo expiration if necessary""" self.slug = slugify(self.slug) self.title = self.title.strip() if self.embargo: if self.status in END_STATUS: default_date = date.today() + timedelta(30) existing_date = self.date_embargo self.date_embargo = default_date if not existing_date else existing_date else: self.date_embargo = None if self.status == 'submitted' and self.date_processing is None: self.date_processing = date.today() # add a reversion comment if possible if 'comment' in kwargs: comment = kwargs.pop('comment') if reversion.revision_context_manager.is_active(): reversion.set_comment(comment) super(FOIARequest, self).save(*args, **kwargs) def is_editable(self): """Can this request be updated?""" return self.status == 'started' def has_crowdfund(self): """Does this request have crowdfunding enabled?""" return bool(self.crowdfund) def is_payable(self): """Can this request be payed for by the user?""" has_open_crowdfund = self.has_crowdfund( ) and not self.crowdfund.expired() has_payment_status = self.status == 'payment' return has_payment_status and not has_open_crowdfund def get_stripe_amount(self): """Output a Stripe Checkout formatted price""" return int(self.price * 100) def is_public(self): """Is this document viewable to everyone""" return self.has_perm(AnonymousUser(), 'view') # Request Sharing and Permissions def has_perm(self, user, perm): """Short cut for checking a FOIA permission""" return user.has_perm('foia.%s_foiarequest' % perm, self) ## Creator def created_by(self, user): """Did this user create this request?""" return self.user == user ## Editors def has_editor(self, user): """Checks whether the given user is an editor.""" user_is_editor = False if self.edit_collaborators.filter(pk=user.pk).exists(): user_is_editor = True return user_is_editor def add_editor(self, user): """Grants the user permission to edit this request.""" if not self.has_viewer(user) and not self.has_editor( user) and not self.created_by(user): self.edit_collaborators.add(user) self.save() logger.info('%s granted edit access to %s', user, self) return def remove_editor(self, user): """Revokes the user's permission to edit this request.""" if self.has_editor(user): self.edit_collaborators.remove(user) self.save() logger.info('%s revoked edit access from %s', user, self) return def demote_editor(self, user): """Reduces the editor's access to that of a viewer.""" self.remove_editor(user) self.add_viewer(user) return ## Viewers def has_viewer(self, user): """Checks whether the given user is a viewer.""" user_is_viewer = False if self.read_collaborators.filter(pk=user.pk).exists(): user_is_viewer = True return user_is_viewer def add_viewer(self, user): """Grants the user permission to view this request.""" if not self.has_viewer(user) and not self.has_editor( user) and not self.created_by(user): self.read_collaborators.add(user) self.save() logger.info('%s granted view access to %s', user, self) return def remove_viewer(self, user): """Revokes the user's permission to view this request.""" if self.has_viewer(user): self.read_collaborators.remove(user) logger.info('%s revoked view access from %s', user, self) self.save() return def promote_viewer(self, user): """Enhances the viewer's access to that of an editor.""" self.remove_viewer(user) self.add_editor(user) return ## Access key def generate_access_key(self): """Generates a random key for accessing the request when it is private.""" key = utils.generate_key(24) self.access_key = key self.save() logger.info('New access key generated for %s', self) return key def public_documents(self): """Get a list of public documents attached to this request""" return self.files.filter(access='public') def first_request(self): """Return the first request text""" try: return self.communications.all()[0].communication except IndexError: return '' def last_comm(self): """Return the last communication""" return self.communications.last() def last_response(self): """Return the most recent response""" return self.communications.filter( response=True).order_by('-date').first() def set_mail_id(self): """Set the mail id, which is the unique identifier for the auto mailer system""" # use raw sql here in order to avoid race conditions uid = int( md5(self.title.encode('utf8') + datetime.now().isoformat()).hexdigest(), 16) % 10**8 mail_id = '%s-%08d' % (self.pk, uid) cursor = connection.cursor() cursor.execute( "UPDATE foia_foiarequest " "SET mail_id = CASE WHEN mail_id='' THEN %s ELSE mail_id END " "WHERE id = %s", [mail_id, self.pk]) # set object's mail id to what is in the database self.mail_id = FOIARequest.objects.get(pk=self.pk).mail_id def get_mail_id(self): """Get the mail id - generate it if it doesn't exist""" if not self.mail_id: self.set_mail_id() return self.mail_id def get_other_emails(self): """Get the other emails for this request as a list""" # Adding blank emails here breaks mailgun backend return [ e for e in fields.email_separator_re.split(self.other_emails) if e ] def get_to_who(self): """Who communications are to""" if self.agency: return self.agency.name else: return '' def get_saved(self): """Get the old model that is saved in the db""" try: return FOIARequest.objects.get(pk=self.pk) except FOIARequest.DoesNotExist: return None def latest_response(self): """How many days since the last response""" response = self.last_response() if response: return (date.today() - response.date.date()).days def processing_length(self): """How many days since the request was set as processing""" days_since = 0 if self.date_processing: days_since = (date.today() - self.date_processing).days return days_since def update(self, anchor=None): """Various actions whenever the request has been updated""" # pylint: disable=unused-argument # Do something with anchor self.updated = True self.save() self.update_dates() def notify(self, action): """ Notify the owner of the request. Notify followers if the request is not under embargo. Mark any existing notifications with the same message as read, to avoid notifying users with duplicated information. """ identical_notifications = ( Notification.objects.for_object(self).get_unread().filter( action__actor_object_id=action.actor_object_id, action__verb=action.verb)) for notification in identical_notifications: notification.mark_read() utils.notify(self.user, action) if self.is_public(): utils.notify(followers(self), action) def submit(self, appeal=False, snail=False, thanks=False): """ The request has been submitted. Notify admin and try to auto submit. There is functionally no difference between appeals and other submissions besides the receiving agency. The only difference between a thanks andother submissions is that we do not set the request status, unless the request requires a proxy. """ # can email appeal if the agency has an appeal agency which has an email address # and can accept emailed appeals can_email_appeal = (appeal and self.agency and self.agency.appeal_agency and self.agency.appeal_agency.email and self.agency.appeal_agency.can_email_appeals) # update email addresses for the request if can_email_appeal: self.email = self.agency.appeal_agency.get_email() self.other_emails = self.agency.appeal_agency.other_emails elif not self.email and self.agency: self.email = self.agency.get_email() self.other_emails = self.agency.other_emails # if agency isnt approved, do not email or snail mail # it will be handled after agency is approved approved_agency = self.agency and self.agency.status == 'approved' can_email = self.email and not appeal and not self.missing_proxy comm = self.last_comm() # if the request can be emailed, email it, otherwise send a notice to the admin # if this is a thanks, send it as normal but do not change the status if not snail and approved_agency and (can_email or can_email_appeal): if appeal and not thanks: self.status = 'appealing' elif self.has_ack() and not thanks: self.status = 'processed' elif not thanks: self.status = 'ack' self._send_msg() self.update_dates() elif self.missing_proxy: # flag for proxy re-submitting self.status = 'submitted' self.date_processing = date.today() task.models.FlaggedTask.objects.create( foia=self, text='This request was filed for an agency requiring a ' 'proxy, but no proxy was available. Please add a suitable ' 'proxy for the state and refile it with a note that the ' 'request is being filed by a state citizen. Make sure the ' 'new request is associated with the original user\'s ' 'account. To add someone as a proxy, change their user type ' 'to "Proxy" and make sure they properly have their state ' 'set on the backend. This message should only appear when ' 'a suitable proxy does not exist.') elif approved_agency: # snail mail it if not thanks: self.status = 'submitted' self.date_processing = date.today() notice = 'n' if self.communications.count() == 1 else 'u' notice = 'a' if appeal else notice comm.delivered = 'mail' comm.save() task.models.SnailMailTask.objects.create(category=notice, communication=comm) elif not thanks: # there should never be a thanks to an unapproved agency # not an approved agency, all we do is mark as submitted self.status = 'submitted' self.date_processing = date.today() self.save() def process_attachments(self, user): """Attach all outbound attachments to the last communication""" attachments = self.pending_attachments.filter( user=user, sent=False, ) comm = self.last_comm() access = 'private' if self.embargo else 'public' for attachment in attachments: file_ = comm.files.create( foia=self, title=os.path.basename(attachment.ffile.name), date=comm.date, source=user.get_full_name(), access=access, ) file_.ffile.name = attachment.ffile.name file_.save() attachments.update(sent=True) def followup(self, automatic=False, show_all_comms=True): """Send a follow up email for this request""" from muckrock.foia.models.communication import FOIACommunication if self.date_estimate and date.today() < self.date_estimate: estimate = 'future' elif self.date_estimate: estimate = 'past' else: estimate = 'none' comm = FOIACommunication.objects.create(foia=self, from_who='MuckRock.com', to_who=self.get_to_who(), date=datetime.now(), response=False, full_html=False, autogenerated=automatic, communication=render_to_string( 'text/foia/followup.txt', { 'request': self, 'estimate': estimate })) if not self.email and self.agency: self.email = self.agency.get_email() self.other_emails = self.agency.other_emails self.save() if self.email: self._send_msg(show_all_comms) else: self.status = 'submitted' self.date_processing = date.today() self.save() comm.delivered = 'mail' comm.save() task.models.SnailMailTask.objects.create(category='f', communication=comm) # Do not self.update() here for now to avoid excessive emails self.update_dates() def appeal(self, appeal_message, user): """Send a followup to the agency or its appeal agency.""" from muckrock.foia.models.communication import FOIACommunication communication = FOIACommunication.objects.create( foia=self, from_who=self.user.get_full_name(), to_who=self.get_to_who(), date=datetime.now(), communication=appeal_message, response=False, full_html=False, autogenerated=False) self.process_attachments(user) self.submit(appeal=True) return communication def pay(self, user, amount): """ Users can make payments for request fees. Upon payment, we create a snail mail task and we set the request to a processing status. Payments are always snail mail, because we need to mail the check to the agency. Since collaborators may make payments, we do not assume the user is the request creator. Returns the communication that was generated. """ from muckrock.foia.models.communication import FOIACommunication from muckrock.task.models import SnailMailTask # We mark the request as processing self.status = 'submitted' self.date_processing = date.today() self.save() # We create the payment communication and a snail mail task for it. payable_to = self.agency.payable_to if self.agency else None comm = FOIACommunication.objects.create( foia=self, from_who='MuckRock.com', to_who=self.get_to_who(), date=datetime.now(), delivered='mail', response=False, full_html=False, autogenerated=False, communication=render_to_string('message/communication/payment.txt', { 'amount': amount, 'payable_to': payable_to })) SnailMailTask.objects.create(communication=comm, category='p', user=user, amount=amount) # We perform some logging and activity generation logger.info('%s has paid %0.2f for request %s', user.username, amount, self.title) utils.new_action(user, 'paid fees', target=self) # We return the communication we generated, in case the caller wants to do anything with it return comm def _send_msg(self, show_all_comms=True): """Send a message for this request as an email or fax""" # self.email should be set before calling this method # get last comm to set delivered and raw_email comm = self.communications.last() subject = comm.subject or self.default_subject() subject = subject[:255] # pylint:disable=attribute-defined-outside-init self.reverse_communications = self.communications.reverse() is_email = not all(c.isdigit() for c in self.email) context = {'request': self, 'show_all_comms': show_all_comms} if is_email: context['reply_link'] = self.get_agency_reply_link(self.email) body = render_to_string( 'text/foia/request_email.txt', context, ) # send the msg if is_email: self._send_email(subject, body, comm) else: self._send_fax(subject, body, comm) comm.subject = subject comm.save() # unblock incoming messages if we send one out self.block_incoming = False self.save() def get_agency_reply_link(self, email): """Get the link for the agency user to log in""" agency = self.agency agency_user_profile = agency.get_user().profile return agency_user_profile.wrap_url( reverse( 'acct-agency-redirect-login', kwargs={ 'agency_slug': agency.slug, 'agency_idx': agency.pk, 'foia_slug': self.slug, 'foia_idx': self.pk, }, ), email=email, ) def _send_email(self, subject, body, comm): """Send the message as an email""" from_addr = self.get_mail_id() cc_addrs = self.get_other_emails() from_email = '%s@%s' % (from_addr, settings.MAILGUN_SERVER_NAME) msg = EmailMultiAlternatives(subject=subject, body=body, from_email=from_email, to=[self.email], bcc=cc_addrs + ['*****@*****.**'], headers={ 'Cc': ','.join(cc_addrs), 'X-Mailgun-Variables': { 'comm_id': comm.pk } }) msg.attach_alternative(linebreaks(escape(body)), 'text/html') # atach all files from the latest communication for file_ in comm.files.all(): name = file_.name() content = file_.ffile.read() mimetype, _ = mimetypes.guess_type(name) if mimetype and mimetype.startswith('text/'): enc = chardet.detect(content)['encoding'] content = content.decode(enc) msg.attach(name, content) msg.send(fail_silently=False) # update communication comm.set_raw_email(msg.message()) comm.delivered = 'email' def _send_fax(self, subject, body, comm): """Send the message as a fax""" # pylint: disable=no-self-use from muckrock.foia.tasks import send_fax send_fax.apply_async(args=[comm.pk, subject, body]) def update_dates(self): """Set the due date, follow up date and days until due attributes""" cal = self.jurisdiction.get_calendar() # first submit if not self.date_submitted: self.date_submitted = date.today() days = self.jurisdiction.get_days() if days: self.date_due = cal.business_days_from(date.today(), days) # updated from mailgun without setting status or submitted if self.status in ['ack', 'processed']: # unpause the count down if self.days_until_due is not None: self.date_due = cal.business_days_from(date.today(), self.days_until_due) self.days_until_due = None self._update_followup_date() # if we are no longer waiting on the agency, do not follow up if self.status not in ['ack', 'processed'] and self.date_followup: self.date_followup = None # if we need to respond, pause the count down until we do if self.status in ['fix', 'payment'] and self.date_due: last_datetime = self.last_comm().date if not last_datetime: last_datetime = datetime.now() self.days_until_due = cal.business_days_between( last_datetime.date(), self.date_due) self.date_due = None self.save() def _update_followup_date(self): """Update the follow up date""" try: new_date = self.last_comm().date.date() + timedelta( self._followup_days()) if self.date_due and self.date_due > new_date: new_date = self.date_due if not self.date_followup or self.date_followup < new_date: self.date_followup = new_date except IndexError: # This request has no communications at the moment, cannot asign a follow up date pass def _followup_days(self): """How many days do we wait until we follow up?""" if self.status == 'ack' and self.jurisdiction: # if we have not at least been acknowledged yet, set the days # to the period required by law jurisdiction_days = self.jurisdiction.get_days() if jurisdiction_days is not None: return jurisdiction_days if self.date_estimate and date.today() < self.date_estimate: # return the days until the estimated date date_difference = self.date_estimate - date.today() return date_difference.days if self.jurisdiction and self.jurisdiction.level == 'f': return 30 else: return 15 def update_tags(self, tags): """Update the requests tags""" tag_set = set() for tag in parse_tags(tags): new_tag, _ = Tag.objects.get_or_create(name=tag) tag_set.add(new_tag) self.tags.set(*tag_set) def user_actions(self, user): '''Provides action interfaces for users''' is_owner = self.created_by(user) is_agency_user = (user.is_authenticated() and user.profile.acct_type == 'agency') can_follow = (user.is_authenticated() and not is_owner and not is_agency_user) is_following = user.is_authenticated() and user in followers(self) is_admin = user.is_staff kwargs = { 'jurisdiction': self.jurisdiction.slug, 'jidx': self.jurisdiction.pk, 'idx': self.pk, 'slug': self.slug } return [ Action(test=not is_agency_user, link=reverse('foia-clone', kwargs=kwargs), title='Clone', desc='Start a new request using this one as a base', class_name='primary'), Action(test=can_follow, link=reverse('foia-follow', kwargs=kwargs), title=('Unfollow' if is_following else 'Follow'), class_name=('default' if is_following else 'primary')), Action( test=self.has_perm(user, 'flag'), title='Get Help', action='flag', desc= u'Something broken, buggy, or off? Let us know and we’ll fix it', class_name='failure modal'), Action(test=is_admin, title='Contact User', action='contact_user', desc=u'Send this request\'s owner an email', class_name='modal'), ] def contextual_request_actions(self, user, can_edit): '''Provides context-sensitive action interfaces for requests''' can_follow_up = can_edit and self.status != 'started' can_appeal = self.has_perm(user, 'appeal') kwargs = { 'jurisdiction': self.jurisdiction.slug, 'jidx': self.jurisdiction.pk, 'idx': self.pk, 'slug': self.slug } return [ Action(test=user.is_staff, link=reverse('foia-admin-fix', kwargs=kwargs), title='Admin Fix', desc='Open the admin fix form', class_name='default'), Action( test=can_edit, title='Get Advice', action='question', desc= u'Get your questions answered by Muckrock’s community of FOIA experts', class_name='modal'), Action(test=can_follow_up, title='Follow Up', action='follow_up', desc='Send a message directly to the agency', class_name='reply'), Action(test=can_appeal, title='Appeal', action='appeal', desc=u'Appeal an agency’s decision', class_name='reply'), ] def total_pages(self): """Get the total number of pages for this request""" pages = self.files.aggregate(Sum('pages'))['pages__sum'] if pages is None: return 0 return pages def has_ack(self): """Has this request been acknowledged?""" return self.communications.filter(response=True).exists() def proxy_reject(self): """Mark this request as being rejected due to a proxy being required""" from muckrock.task.models import FlaggedTask # mark the agency as requiring a proxy going forward self.agency.requires_proxy = True self.agency.save() # mark to re-file with a proxy FlaggedTask.objects.create( foia=self, text='This request was rejected as requiring a proxy; please refile' ' it with one of our volunteers names and a note that the request is' ' being filed by a state citizen. Make sure the new request is' ' associated with the original user\'s account. To add someone as' ' a proxy, change their user type to "Proxy" and make sure they' ' properly have their state set on the backend. This message should' ' only appear the first time an agency rejects a request for being' ' from an out-of-state resident.') self.notes.create( author=User.objects.get(username='******'), note='The request has been rejected with the agency stating that ' 'you must be a resident of the state. MuckRock is working with our ' 'in-state volunteers to refile this request, and it should appear ' 'in your account within a few days.', ) def default_subject(self): """Make a subject line for a communication for this request""" law_name = self.jurisdiction.get_law_name() if self.tracking_id: return 'RE: %s Request #%s' % (law_name, self.tracking_id) elif self.communications.count() > 1: return 'RE: %s Request: %s' % (law_name, self.title) else: return '%s Request: %s' % (law_name, self.title) class Meta: # pylint: disable=too-few-public-methods ordering = ['title'] verbose_name = 'FOIA Request' app_label = 'foia' permissions = ( ('view_foiarequest', 'Can view this request'), ('embargo_foiarequest', 'Can embargo request to make it private'), ('embargo_perm_foiarequest', 'Can embargo a request permananently'), ('crowdfund_foiarequest', 'Can start a crowdfund campaign for the request'), ('appeal_foiarequest', 'Can appeal the requests decision'), ('thank_foiarequest', 'Can thank the FOI officer for their help'), ('flag_foiarequest', 'Can flag the request for staff attention'), ('followup_foiarequest', 'Can send a manual follow up'), ('agency_reply_foiarequest', 'Can send a direct reply'), ('upload_attachment_foiarequest', 'Can upload an attachment'), )
class NewsPost(models.Model): # user = UserForeignKey( # auto_user_add=True, on_delete=models.CASCADE) author = models.ForeignKey(Profile, null=True, blank=True, on_delete=models.CASCADE) title = models.CharField(verbose_name='title', max_length=100) over_view = models.TextField(blank=True, null=True) content = models.TextField(verbose_name='') tags = TaggableManager() comment_count = models.IntegerField(default=0) thumbnail = models.ImageField( default='/News_Pictures/element5-digital-WCPg9ROZbM0-unsplash.jpg', upload_to='News_Pictures', null=True, blank=True) # categories = models.ManyToManyField(Category) categories = models.ManyToManyField(Categories) featured = models.BooleanField(default=True) previous_post = models.ForeignKey('self', on_delete=models.SET_NULL, related_name='previous', null=True, blank=True) next_post = models.ForeignKey('self', on_delete=models.SET_NULL, related_name='next', null=True, blank=True) liked = models.ManyToManyField(Profile, related_name='like', default=None, blank=True) publish = models.DateField(auto_now=False, auto_now_add=False) words_count = models.IntegerField(default=0) read_time = models.TimeField(null=True, blank=True) update = models.DateTimeField(auto_now=True, auto_now_add=False) date_created = models.DateTimeField(auto_now=False, auto_now_add=True) time_stamp = models.DateTimeField(auto_now_add=True) objects = PostManager() def __str__(self): return self.title def get_absolute_url(self): return reverse("news:post-details", kwargs={'newspost_id': self.id}) def get_over_view_mark_down(self): over_view = self.over_view markdown_text = markdown(over_view) return mark_safe(markdown_text) def get_mark_down(self): content = self.content markdown_text = markdown(content) return mark_safe(markdown_text) def get_tag_url(self): return reverse("news:post-by-tag", kwargs={'newspost_id': self.id}) def get_like_url(self): return reverse("news:like", kwargs={'newspost_id': self.id}) def get_update_url(self): return reverse("news:update-post", kwargs={'newspost_id': self.id}) def get_delete_url(self): return reverse("news:delete-post", kwargs={'newspost_id': self.id}) @property def get_comments(self): return self.comments.all().order_by('-time_stamp') @property def num_liked(self): return self.liked.all().count() @property def view_count(self): return PostView.objects.filter(post=self).count()
class FOIAComposer(models.Model): """A FOIA request composer""" # pylint: disable=too-many-instance-attributes user = models.ForeignKey(User, on_delete=models.PROTECT, related_name="composers") # only null for initial migration organization = models.ForeignKey( "organization.Organization", on_delete=models.PROTECT, related_name="composers", null=True, ) title = models.CharField(max_length=255) slug = models.SlugField(max_length=255) status = models.CharField(max_length=10, choices=STATUS, default="started") agencies = models.ManyToManyField("agency.Agency", related_name="composers") requested_docs = models.TextField(blank=True) edited_boilerplate = models.BooleanField(default=False) datetime_created = models.DateTimeField(default=timezone.now, db_index=True) datetime_submitted = models.DateTimeField(blank=True, null=True, db_index=True) embargo = models.BooleanField(default=False) permanent_embargo = models.BooleanField(default=False) parent = models.ForeignKey( "self", blank=True, null=True, on_delete=models.SET_NULL, help_text="The composer this was cloned from, if cloned", ) # for refunding requests if necessary num_monthly_requests = models.PositiveSmallIntegerField(default=0) num_reg_requests = models.PositiveSmallIntegerField(default=0) # for delayed submission delayed_id = models.CharField(blank=True, max_length=255) objects = FOIAComposerQuerySet.as_manager() tags = TaggableManager(through=TaggedItemBase, blank=True) class Meta: verbose_name = "FOIA Composer" def __str__(self): return self.title def save(self, *args, **kwargs): """Set title and slug on save""" # pylint: disable=signature-differs self.title = self.title.strip() or "Untitled" self.slug = slugify(self.title) or "untitled" super(FOIAComposer, self).save(*args, **kwargs) def delete(self, *args, **kwargs): """Resolve any pending new agency tasks""" # pylint: disable=signature-differs for agency in self.agencies.filter(status="pending"): if agency.composers.count() == 1: agency.delete() super(FOIAComposer, self).delete(*args, **kwargs) def get_absolute_url(self): """The url for this object""" return reverse("foia-composer-detail", kwargs={ "slug": self.slug, "idx": self.pk }) def submit(self, contact_info=None, no_proxy=False): """Submit a composer to create the requests""" # pylint: disable=import-outside-toplevel from muckrock.foia.tasks import composer_create_foias, composer_delayed_submit num_requests = self.agencies.count() request_count = self.organization.make_requests(num_requests) self.num_reg_requests = request_count["regular"] self.num_monthly_requests = request_count["monthly"] self.status = "submitted" self.datetime_submitted = timezone.now() self.save() if num_requests == 1: # if only one request, create it immediately so we can redirect there composer_create_foias(self.pk, contact_info, no_proxy) else: # otherwise do it delayed so the page doesn't risk timing out composer_create_foias.delay(self.pk, contact_info, no_proxy) # if num_requests is less than the multi-review amount, we will approve # the request right away, other wise we create a multirequest task # if the request contains a moderated keyword, it will also not be # approved approve = (num_requests < settings.MULTI_REVIEW_AMOUNT and not self.needs_moderation()) result = composer_delayed_submit.apply_async( args=(self.pk, approve, contact_info), countdown=COMPOSER_SUBMIT_DELAY) self.delayed_id = result.id self.save() def needs_moderation(self): """Check for moderated keywords""" for keyword in config.MODERATION_KEYWORDS.split("\n"): if keyword in self.title or keyword in self.requested_docs: return True return False def approved(self, contact_info=None): """A pending composer is approved for sending to the agencies""" for foia in self.foias.all(): foia.submit(contact_info=contact_info) self.status = "filed" self.save() def has_perm(self, user, perm): """Short cut for checking a FOIA composer permission""" return user.has_perm("foia.%s_foiacomposer" % perm, self) def return_requests(self, num_requests=None): """Return requests to the composer's author""" if num_requests is None: # if no num_requests passed in, refund all requests return_amts = { "regular": self.num_reg_requests, "monthly": self.num_monthly_requests, } else: return_amts = self._calc_return_requests(num_requests) self._return_requests(return_amts) @transaction.atomic def _return_requests(self, return_amts): """Helper method for return requests Does the actually returning """ self.num_reg_requests = F("num_reg_requests") - Least( return_amts["regular"], F("num_reg_requests")) self.num_monthly_requests = F("num_monthly_requests") - Least( return_amts["monthly"], F("num_monthly_requests")) self.save() self.organization.return_requests(return_amts) def _calc_return_requests(self, num_requests): """Determine how many of each type of request to return""" used = [self.num_reg_requests, self.num_monthly_requests] ret = [] while num_requests: try: num_used = used.pop(0) except IndexError: ret.append(num_requests) break else: num_ret = min(num_used, num_requests) num_requests -= num_ret ret.append(num_ret) ret_dict = dict( zip_longest(["regular", "monthly", "extra"], ret, fillvalue=0)) ret_dict["regular"] += ret_dict.pop("extra") return ret_dict def revokable(self): """Is this composer revokable?""" return (self.delayed_id != "" and self.datetime_submitted > timezone.now() - timedelta(seconds=COMPOSER_EDIT_DELAY) and self.status == "submitted") def revoke(self): """Revoke a submitted composer""" # pylint: disable=import-outside-toplevel from muckrock.foia.signals import foia_file_delete_s3 current_app.control.revoke(self.delayed_id) self.status = "started" self.delayed_id = "" self.datetime_submitted = None disconnect_kwargs = { "signal": post_delete, "receiver": foia_file_delete_s3, "sender": FOIAFile, "dispatch_uid": "muckrock.foia.signals.file_delete_s3", } with TempDisconnectSignal(**disconnect_kwargs): self.foias.all().delete() self.pending_attachments.update(sent=False) self.return_requests() self.save() def attachments_over_size_limit(self, user): """Are the pending attachments for this composer over the size limit?""" total_size = sum( a.ffile.size for a in self.pending_attachments.filter(user=user, sent=False)) return total_size > settings.MAX_ATTACHMENT_TOTAL_SIZE
def setUp(self): tags = TaggableManager() settings.SIGNALS = 'off' dc2 = DcFactory.create(name='Tokyo', symbol="dc2") dc1 = DcFactory.create(name="Bilbao", symbol="dc1") cluster1 = LogicalClusterFactory.create(id=1, name='cluster1_siteA_test') cluster2 = LogicalClusterFactory.create(id=2, name='cluster2_siteB_test') time_profile = TimeProfile.objects.create(name='generic', max_connections=1, connect_timeout=0.5, first_byte_timeout=0.1, between_bytes_timeout=1) non_active_active_routed_by_path = DirectorFactory.create( name='first_service', route_expression='/first', active_active=False, mode='round-robin', protocol='https', remove_path=False, time_profile=time_profile) active_active_remove_path = DirectorFactory.create( name='second_service', mode='random', route_expression='/second', remove_path=True) active_active_routed_by_domain = DirectorFactory.create( name='third_service', mode='random', router='req.http.host', route_expression='third.service.org') active_active_with_too_long_name = DirectorFactory.create( name='fourth_director_which_has_a_ridiculously_long_name', mode='random', router='req.http.host', route_expression='unusual.name.org') active_active_absent_in_second_cluster = DirectorFactory.create( name='fifth_director_only_cluster1_siteA_test', route_expression='/fifth') active_active_hashing_by_cookie = DirectorFactory.create( name='sixth_director_hashing_by_cookie', route_expression='/sixth', mode='hash', hashing_policy='req.http.cookie') active_active_hashing_by_url = DirectorFactory.create( name='seventh_director_hashing_by_url', route_expression='/seventh', mode='hash', hashing_policy='req.url') """ connect directors to clusters """ non_active_active_routed_by_path.cluster.add(1, 2) active_active_remove_path.cluster.add(1, 2) active_active_routed_by_domain.cluster.add(1, 2) active_active_with_too_long_name.cluster.add(1, 2) active_active_absent_in_second_cluster.cluster.add(1) active_active_hashing_by_cookie.cluster.add(1, 2) active_active_hashing_by_url.cluster.add(1, 2) BackendFactory.create(address='127.0.1.1', dc=dc2, director=non_active_active_routed_by_path, inherit_time_profile=True) BackendFactory.create(address='127.0.2.1', dc=dc2, director=active_active_remove_path) BackendFactory.create(address='127.4.2.1', dc=dc1, director=active_active_remove_path) BackendFactory.create(address='127.8.2.1', dc=dc1, director=active_active_routed_by_domain) BackendFactory.create(address='127.9.255.254', port=65535, dc=dc1, director=active_active_with_too_long_name) BackendFactory.create(address='127.9.2.1', dc=dc1, director=active_active_absent_in_second_cluster) BackendFactory.create(address='127.10.2.1', dc=dc1, director=active_active_hashing_by_cookie) BackendFactory.create(address='127.11.2.1', dc=dc1, director=active_active_hashing_by_url) canary_backend = BackendFactory.create( address='127.4.2.2', dc=dc1, director=active_active_remove_path, weight=0) canary_backend.tags.add('canary') template_v3 = VclTemplate.objects.create( name='new', content='<VCL/>\n## #{vcl_variable} ##', version='3.0') template_v4 = VclTemplate.objects.create(name='new-v4', content='<VCL/>', version='4.0') vcl_variable = VclVariable.objects.create(key='vcl_variable', value='vcl_variable_content', cluster=cluster1) self.varnish = VarnishServer.objects.create(ip='127.0.0.1', dc=dc2, template=template_v3, cluster=cluster1) self.varnish_dc1 = VarnishServer.objects.create(ip='127.4.0.1', dc=dc1, template=template_v3, cluster=cluster1) self.varnish4 = VarnishServer.objects.create(ip='127.0.0.2', dc=dc2, template=template_v4, cluster=cluster2) self.varnish3_canary = VarnishServer.objects.create( ip='127.0.0.3', dc=dc2, template=template_v3, cluster=cluster1, is_canary=True) self.varnish4_canary = VarnishServer.objects.create( ip='127.0.0.4', dc=dc2, template=template_v4, cluster=cluster2, is_canary=True)
class Post(models.Model): "Represents a post in a forum" # Post statuses. PENDING, OPEN, OFFTOPIC, CLOSED, DELETED = range(5) STATUS_CHOICES = [(PENDING, "Pending"), (OPEN, "Open"), (OFFTOPIC, "Off topic"), (CLOSED, "Closed"), (DELETED, "Deleted")] # Question types. Answers should be listed before comments. QUESTION, ANSWER, JOB, FORUM, PAGE, BLOG, COMMENT, DATA, TUTORIAL, BOARD, TOOL, NEWS = range( 12) # Valid post types. TYPE_CHOICES = [(QUESTION, "Question"), (ANSWER, "Answer"), (COMMENT, "Comment"), (JOB, "Job"), (FORUM, "Forum"), (TUTORIAL, "Tutorial"), (DATA, "Data"), (PAGE, "Page"), (TOOL, "Tool"), (NEWS, "News"), (BLOG, "Blog"), (BOARD, "Bulletin Board")] TOP_LEVEL = {QUESTION, JOB, FORUM, BLOG, TUTORIAL, TOOL, NEWS} # Possile spam states. SPAM, NOT_SPAM, DEFAULT, SUSPECT = range(4) SPAM_CHOICES = [(SPAM, "Spam"), (NOT_SPAM, "Not spam"), (SUSPECT, "Quarantine"), (DEFAULT, "Default")] # Spam labeling. spam = models.IntegerField(choices=SPAM_CHOICES, default=DEFAULT) # Spam score stores relative likely hood this post is spam. spam_score = models.FloatField(default=0) # Post status: open, closed, deleted. status = models.IntegerField(choices=STATUS_CHOICES, default=OPEN, db_index=True) # The type of the post: question, answer, comment. type = models.IntegerField(choices=TYPE_CHOICES, db_index=True) # Post title. title = models.CharField(max_length=200, null=False, db_index=True) # The user that originally created the post. author = models.ForeignKey(User, on_delete=models.CASCADE) # The user that edited the post most recently. lastedit_user = models.ForeignKey(User, related_name='editor', null=True, on_delete=models.CASCADE) # The user that last contributed to the thread. last_contributor = models.ForeignKey(User, related_name='contributor', null=True, on_delete=models.CASCADE) # Store users contributing to the thread as "tags" to preform_search later. thread_users = models.ManyToManyField(User, related_name="thread_users") # Indicates the information value of the post. rank = models.FloatField(default=0, blank=True, db_index=True) # This post has been indexed by the search engine. indexed = models.BooleanField(default=False) # Used for efficiency #is_public_toplevel = models.BooleanField(default=False) # Show that post is top level is_toplevel = models.BooleanField(default=False, db_index=True) # Indicates whether the post has accepted answer. answer_count = models.IntegerField(default=0, blank=True, db_index=True) # The number of accepted answers. accept_count = models.IntegerField(default=0, blank=True) # The number of replies for thread. reply_count = models.IntegerField(default=0, blank=True, db_index=True) # The number of comments that a post has. comment_count = models.IntegerField(default=0, blank=True) # Number of upvotes for the post vote_count = models.IntegerField(default=0, blank=True, db_index=True) # The total numbers of votes for a top-level post. thread_votecount = models.IntegerField(default=0, db_index=True) # The number of views for the post. view_count = models.IntegerField(default=0, blank=True, db_index=True) # Bookmark count. book_count = models.IntegerField(default=0) # How many people follow that thread. subs_count = models.IntegerField(default=0) # Post creation date. creation_date = models.DateTimeField(db_index=True) # Post last edit date. lastedit_date = models.DateTimeField(db_index=True) # Sticky posts go on top. sticky = models.BooleanField(default=False) # This will maintain the ancestor/descendant relationship bewteen posts. root = models.ForeignKey('self', related_name="descendants", null=True, blank=True, on_delete=models.SET_NULL) # This will maintain parent/child relationships between posts. parent = models.ForeignKey('self', null=True, blank=True, related_name='children', on_delete=models.SET_NULL) # This is the text that the user enters. content = models.TextField(default='') # This is the HTML that gets displayed. html = models.TextField(default='') # The tag value is the canonical form of the post's tags tag_val = models.CharField(max_length=100, default="", blank=True) # The tag set is built from the tag string and used only for fast filtering tags = TaggableManager() # What site does the post belong to. site = models.ForeignKey(Site, null=True, on_delete=models.SET_NULL) # Unique id for the post. uid = models.CharField(max_length=32, unique=True, db_index=True) objects = PostManager() def parse_tags(self): return [tag.lower() for tag in self.tag_val.split(",") if tag] @property def get_votecount(self): if self.is_toplevel: return self.thread_votecount return self.vote_count def title_prefix(self): prefix = "" if self.is_spam: prefix = "Spam:" elif self.suspect_spam: prefix = "Quarantined: " elif not self.is_open or not self.is_question: prefix = f"{self.get_type_display()}:" if self.is_open else f"{self.get_status_display()}:" return prefix @property def suspect_spam(self): return self.spam == self.SUSPECT @property def is_open(self): return self.status == Post.OPEN and not self.is_spam and not self.suspect_spam def recompute_scores(self): # Recompute answers count if self.type == Post.ANSWER: answer_count = Post.objects.valid_posts(root=self.root, type=Post.ANSWER).count() Post.objects.filter(pk=self.parent_id).update( answer_count=answer_count) reply_count = Post.objects.valid_posts(root=self.root).exclude( pk=self.root.pk).count() print(reply_count) Post.objects.filter(pk=self.root.id).update(reply_count=reply_count) def json_data(self): data = { 'id': self.id, 'uid': self.uid, 'title': self.title, 'type': self.get_type_display(), 'type_id': self.type, 'creation_date': util.datetime_to_iso(self.creation_date), 'lastedit_date': util.datetime_to_iso(self.lastedit_date), 'author_uid': self.author.profile.uid, 'lastedit_user_uid': self.lastedit_user.profile.uid, 'author': self.author.profile.name, 'status': self.get_status_display(), 'status_id': self.status, 'thread_score': self.thread_votecount, 'rank': self.rank, 'vote_count': self.vote_count, 'view_count': self.view_count, 'reply_count': self.reply_count, 'comment_count': self.comment_count, 'book_count': self.book_count, 'subs_count': self.subs_count, 'answer_count': self.root.reply_count, 'has_accepted': self.has_accepted, 'parent_id': self.parent.id, 'root_id': self.root_id, 'xhtml': self.html, 'content': self.content, 'tag_val': self.tag_val, 'url': f'{settings.PROTOCOL}://{settings.SITE_DOMAIN}{self.get_absolute_url()}', } return data @property def is_question(self): return self.type == Post.QUESTION @property def is_job(self): return self.type == Post.JOB @property def is_deleted(self): return self.status == Post.DELETED @property def not_spam(self): return self.spam == Post.NOT_SPAM @property def has_accepted(self): return bool(self.accept_count) def num_lines(self, offset=0): """ Return number of lines in post content """ return len(self.content.split("\n")) + offset @property def is_spam(self): return self.spam == self.SPAM @property def is_comment(self): return self.type == Post.COMMENT @property def is_answer(self): return self.type == Post.ANSWER def get_absolute_url(self): url = reverse("post_view", kwargs=dict(uid=self.root.uid)) return url if self.is_toplevel else "%s#%s" % (url, self.uid) def high_spam_score(self, threshold=None): threshold = threshold or settings.SPAM_THRESHOLD return (self.spam_score > threshold) or self.is_spam or self.author.profile.low_rep def save(self, *args, **kwargs): # Needs to be imported here to avoid circular imports. from biostar.forum import markdown self.lastedit_user = self.lastedit_user or self.author self.creation_date = self.creation_date or util.now() self.lastedit_date = self.lastedit_date or util.now() self.last_contributor = self.lastedit_user # Sanitize the post body. self.html = markdown.parse(self.content, post=self, clean=True, escape=False) self.tag_val = self.tag_val.replace(' ', '') # Default tags self.tag_val = self.tag_val or "tag1,tag2" # Set the top level state of the post. self.is_toplevel = self.type in Post.TOP_LEVEL # This will trigger the signals super(Post, self).save(*args, **kwargs) def __str__(self): return "%s: %s (pk=%s)" % (self.get_type_display(), self.title, self.pk) def update_parent_counts(self): """ Update the counts for the parent and root """ descendants = Post.objects.filter(root=self.root).exclude( Q(pk=self.root.pk) | Q(status=Post.DELETED) | Q(spam=Post.SPAM)) answer_count = descendants.filter(type=Post.ANSWER).count() comment_count = descendants.filter(type=Post.COMMENT).count() reply_count = descendants.count() # Update the root reply, answer, and comment counts. Post.objects.filter(pk=self.root.pk).update( reply_count=reply_count, answer_count=answer_count, comment_count=comment_count) children = Post.objects.filter(parent=self.parent).exclude( pk=self.parent.pk) com_count = children.filter(type=Post.COMMENT).count() # Update parent reply, answer, and comment counts. Post.objects.filter(pk=self.parent.pk, is_toplevel=False).update( comment_count=com_count, answer_count=0, reply_count=children.count()) @property def css(self): # Used to simplify CSS rendering. status = self.get_status_display() if self.is_spam: return "spam" if self.suspect_spam: return "quarantine" return f"{status}".lower() @property def accepted_class(self): if self.status == Post.DELETED: return "deleted" if self.has_accepted and not self.is_toplevel: return "accepted" return "" @property def age_in_days(self): delta = util.now() - self.creation_date return delta.days
class Material(models.Model): ''' Materials to be used for products/bill of materials''' MAT_TYPE_SELECTIONS = ( ('TIM', 'Time'), ('FAB', 'Fabric'), ('ACC', 'Accesories'), ('FIL', 'Filling'), ('SMA', 'Small Materials'), ) UNIT_USAGE_SELECTIONS = ( ('HU', 'Hours'), ('PI', 'Pieces'), ('ME', 'Meters'), ('KG', 'Kilograms') ) UNIT_PURCHASE_SELECTIONS = ( ('MO', 'Months'), ('PC', 'Pieces'), ('ME', 'Meters'), ('RO', 'Rolls'), ('BO', 'Box'), ('BA', 'Bags'), ) sku = models.CharField(max_length=50) sku_supplier = models.CharField(max_length=50) name = models.CharField(max_length=50) name_cz = models.CharField(max_length=50, blank=True, null=True) description = models.TextField(blank=True, null=True) mat_type = models.CharField(max_length=3, choices=MAT_TYPE_SELECTIONS, verbose_name="Material type") roll_width = models.CharField(max_length=3, verbose_name='Roll width in cm', blank=True, null=True) fabric_width = models.CharField(max_length=3, verbose_name='Fabric width in cm', blank=True, null=True) cost_per_usage_unit = models.FloatField() unit_usage = models.CharField(max_length=2, choices=UNIT_USAGE_SELECTIONS, verbose_name="Usage unit") unit_purchase = models.CharField(max_length=2, choices=UNIT_PURCHASE_SELECTIONS, verbose_name="Purchase unit") unit_usage_in_purchase = models.FloatField(verbose_name="Number of usage units in purchase unit") est_delivery_time = models.CharField(max_length=100, blank=True, null=True) supplier = models.ForeignKey(Relation, limit_choices_to={'is_supplier': True}) sample_box_number = models.IntegerField() tags = TaggableManager(blank=True) class Meta: ordering = ('name',) def __unicode__(self): return self.name @property def usage_units_on_stock(self): '''Show the stock status on each location''' stock_status = {} for location in StockLocation.objects.all(): try: item_in_location = StockLocationItem.objects.get(location=location, material=self) stock_status[location.name] = item_in_location.quantity_in_stock except StockLocationItem.DoesNotExist: stock_status[location.name] = 0 return stock_status
class ArticlePost(models.Model): # 文章作者。参数 on_delete 用于指定数据删除的方式 author = models.ForeignKey(User, on_delete=models.CASCADE) # 文章标题。models.CharField 为字符串字段,用于保存较短的字符串,比如标题 title = models.CharField(max_length=100) # 文章正文。保存大量文本使用 TextField body = models.TextField() # 文章创建时间。参数 default=timezone.now 指定其在创建数据时将默认写入当前的时间 created = models.DateTimeField(default=timezone.now) # 文章更新时间。参数 auto_now=True 指定每次数据更新时自动写入当前时间 updated = models.DateTimeField(auto_now=True) total_views = models.PositiveIntegerField(default=0) # 标题图 avatar = models.ImageField(upload_to='article/%Y%m%d/', blank=True) # avatar = ProcessedImageField( # upload_to='article/%Y%m%d', # processors=[ResizeToFit(width=400)], # format='JPEG', # options={'quality': 100}, # ) # 文章栏目的 “一对多” 外键 column = models.ForeignKey(ArticleColumn, null=True, blank=True, on_delete=models.CASCADE, related_name='article') tags = TaggableManager(blank=True) # 新增点赞数统计 likes = models.PositiveIntegerField(default=0) # 内部类 class Meta 用于给 model 定义元数据 class Meta: # ordering 指定模型返回的数据的排列顺序 # '-created' 表明数据应该以倒序排列 ordering = ('-created', ) # 函数 __str__ 定义当调用对象的 str() 方法时的返回值内容 def __str__(self): # return self.title 将文章标题返回 return self.title def get_absolute_url(self): return reverse('article:article_detail', args=[self.id]) def save(self, *args, **kwargs): article = super(ArticlePost, self).save(*args, **kwargs) # 固定宽度缩放图片大小 if self.avatar and not kwargs.get('update_fields'): image = Image.open(self.avatar) (x, y) = image.size new_x = 400 new_y = int(new_x * (y / x)) resized_image = image.resize((new_x, new_y), Image.ANTIALIAS) resized_image.save(self.avatar.path) return article
class ResearchProject(models.Model): """ Research projects are entities for grouping collections (and by that resources) for future processing. Access to various operations on projects are described by :class:`ResearchProjectRole` objects. Research project is a kind of a middle-step between resources and classifications. """ name = models.CharField(max_length=255, unique=True) acronym = models.CharField(max_length=10, unique=True) description = SafeTextField(max_length=2000, null=True, blank=True) abstract = SafeTextField(max_length=2000, null=True, blank=True) methods = SafeTextField(max_length=2000, null=True, blank=True) owner = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='research_projects') collections = models.ManyToManyField('storage.Collection', through='ResearchProjectCollection', blank=True, related_name='research_projects') date_created = models.DateTimeField(auto_now_add=True) status = models.NullBooleanField(choices=ResearchProjectStatus.CHOICES) status_date = models.DateTimeField(blank=True, null=True, editable=False) keywords = TaggableManager() objects = ResearchProjectManager() class Meta: ordering = ['-status_date'] def __unicode__(self): return unicode(self.acronym) def get_roles(self): """Return mapping between users and their roles: .. code-block:: python { <user>: [<role_name>, <role_name>, ...], <user>: [<role_name>, <role_name>, ...], ... } """ role_map = {} roles = self.project_roles.all() for role in roles: role_map.setdefault(role.user, []).append(role) return role_map def get_user_roles(self, user=None): """Returns a tuple of project roles for given user. :param: user :class:`auth.User` instance for which the roles are determined :return: list of role names of given user withing the project """ user = user or get_current_user() roles = self.project_roles.filter(user=user) return [role.get_name_display() for role in roles] def get_user_roles_with_profiles(self): return self.project_roles.all().select_related('user', 'user__userprofile') def can_update(self, user=None): """Determines whether given user can update the project. :param: user :class:`auth.User` instance for which test is made :return: True if user can update project, False otherwise """ user = user or get_current_user() return self.status is True and user.is_authenticated() and ( self.owner == user or self.project_roles.filter( user=user, name__in=ResearchProjectRoleType.EDIT).exists()) def can_delete(self, user=None): """Determines whether given user can delete the project. :param: user :class:`auth.User` instance for which test is made :return: True if user can delete the project, False otherwise """ user = user or get_current_user() return self.status is True and user.is_authenticated() and ( self.owner == user or self.project_roles.filter( user=user, name__in=ResearchProjectRoleType.DELETE).exists()) def can_view(self, user=None): """Determines whether given user can see the details of a project. :param: user :class:`auth.User` instance for which test is made :return: True if user can see the details of the project, False otherwise """ user = user or get_current_user() return self.status is True and user.is_authenticated() and ( self.owner == user or self.project_roles.filter(user=user).exists()) def can_create_classification_project(self, user=None): """Determine if user can use this project to create classification project""" return self.can_view(user=user) def get_absolute_url(self): """Return url of research project details""" return reverse('research:project_detail', kwargs={'pk': self.pk}) def save(self, **kwargs): """ If project has been accepted, then accept date is set, also when project is created, two notifications are sent: * to user, that project has been created * to admins, that project has been created and waiting for approve or decline """ if (self.status is not None and self.status_date is None): self.status_date = datetime_aware() super(ResearchProject, self).save(**kwargs) def get_admin_url(self): """ Get full url to the research project change view in admin based on project.pk """ request = get_current_request() return request.build_absolute_uri( reverse('admin:research_researchproject_change', args=(self.pk, ))) def send_create_message(self): """Notify all django admins about new project using :class:`apps.messaging.models.Message` (application messaging) """ User = get_user_model() recipients = User.objects.filter(is_active=True, is_superuser=True) body_template = ( 'New research project has been created. You can approve or reject it ' 'by changing its status at:\n' '{url}').format(url=reverse( 'admin:research_researchproject_change', args=(self.pk, ))) for recipient in recipients: Message.objects.create( subject=(u"New research project: <strong>{name}</strong> " u"created").format(name=self.name), text=body_template, user_from=self.owner, user_to=recipient, date_sent=datetime_aware(), message_type=MessageType.RESEARCH_PROJECT_CREATED)
class Profile(AbstractUser): """Fully featured Geonode user""" organization = models.CharField( _('Organization Name'), max_length=255, blank=True, null=True, help_text=_('name of the responsible organization')) profile = models.TextField(_('Profile'), null=True, blank=True, help_text=_('introduce yourself')) position = models.CharField( _('Position Name'), max_length=255, blank=True, null=True, help_text=_('role or position of the responsible person')) voice = models.CharField(_('Voice'), max_length=255, blank=True, null=True, help_text=_( 'telephone number by which individuals can speak to the responsible organization or individual')) fax = models.CharField(_('Facsimile'), max_length=255, blank=True, null=True, help_text=_( 'telephone number of a facsimile machine for the responsible organization or individual')) delivery = models.CharField( _('Delivery Point'), max_length=255, blank=True, null=True, help_text=_('physical and email address at which the organization or individual may be contacted')) city = models.CharField( _('City'), max_length=255, blank=True, null=True, help_text=_('city of the location')) area = models.CharField( _('Administrative Area'), max_length=255, blank=True, null=True, help_text=_('state, province of the location')) zipcode = models.CharField( _('Postal Code'), max_length=255, blank=True, null=True, help_text=_('ZIP or other postal code')) country = models.CharField( choices=COUNTRIES, max_length=3, blank=True, null=True, help_text=_('country of the physical address')) keywords = TaggableManager(_('keywords'), blank=True, help_text=_( 'commonly used word(s) or formalised word(s) or phrase(s) used to describe the subject \ (space or comma-separated')) def get_absolute_url(self): return reverse('profile_detail', args=[self.username, ]) def __unicode__(self): return u"%s" % (self.username) def class_name(value): return value.__class__.__name__ USERNAME_FIELD = 'username' def group_list_public(self): return GroupProfile.objects.exclude(access="private").filter(groupmember__user=self) def group_list_all(self): return GroupProfile.objects.filter(groupmember__user=self) def group_list_with_default_check(self): group_list = GroupProfile.objects.filter(groupmember__user=self) default_group = GroupProfile.objects.get(slug='default') if len(group_list) > 1 and default_group in group_list: return group_list.exclude(slug='default') else: return group_list @property def is_manager_of_any_group(self): return GroupProfile.objects.filter(groupmember__user=self, groupmember__role="manager").exists() @property def is_member_of_any_group(self): return GroupProfile.objects.filter(groupmember__user=self, groupmember__role="member").exists() def keyword_list(self): """ Returns a list of the Profile's keywords. """ return [kw.name for kw in self.keywords.all()] @property def name_long(self): if self.first_name and self.last_name: return '%s %s (%s)' % (self.first_name, self.last_name, self.username) elif (not self.first_name) and self.last_name: return '%s (%s)' % (self.last_name, self.username) elif self.first_name and (not self.last_name): return '%s (%s)' % (self.first_name, self.username) else: return self.username @property def location(self): return format_address(self.delivery, self.zipcode, self.city, self.area, self.country)
def test_internal_type_is_manytomany(self): self.assertEqual(TaggableManager().get_internal_type(), 'ManyToManyField')
@property def post_template(self): return 'tumblelog/post/%s.html' % slugify(self.__class__.__name__) @property def rss_template(self): return [ 'tumblelog/rss/%s.html' % slugify(self.__class__.__name__), self.post_template, ] # Add the django-taggit manager, if taggit is installed if USE_TAGGIT: from taggit.managers import TaggableManager taggit_manager = TaggableManager() taggit_manager.contribute_to_class(BasePostType, 'tags') class BaseOembedPostType(BasePostType): """ Abstract post type base classes whose subclasses retrieve data from an oEmbed endpoint. """ caption = models.TextField(_('Caption'), blank=True, null=True, help_text=TEXTFIELD_HELP_TEXT ) version = models.CharField(_('oEmbed Version'), max_length=3, null=True, \ blank=True, editable=True)
class PrimordialModel(HasEditsMixin, CreatedModifiedModel): """ Common model for all object types that have these standard fields must use a subclass CommonModel or CommonModelNameNotUnique though as this lacks a name field. """ class Meta: abstract = True description = models.TextField( blank=True, default='', ) created_by = models.ForeignKey( 'auth.User', related_name='%s(class)s_created+', default=None, null=True, editable=False, on_delete=models.SET_NULL, ) modified_by = models.ForeignKey( 'auth.User', related_name='%s(class)s_modified+', default=None, null=True, editable=False, on_delete=models.SET_NULL, ) tags = TaggableManager(blank=True) def __init__(self, *args, **kwargs): r = super(PrimordialModel, self).__init__(*args, **kwargs) if self.pk: self._prior_values_store = self._get_fields_snapshot() else: self._prior_values_store = {} return r def save(self, *args, **kwargs): update_fields = kwargs.get('update_fields', []) user = get_current_user() if user and not user.id: user = None if not self.pk and not self.created_by: self.created_by = user if 'created_by' not in update_fields: update_fields.append('created_by') # Update modified_by if any editable fields have changed new_values = self._get_fields_snapshot() if (not self.pk and not self.modified_by) or self._values_have_edits(new_values): self.modified_by = user if 'modified_by' not in update_fields: update_fields.append('modified_by') super(PrimordialModel, self).save(*args, **kwargs) self._prior_values_store = new_values def clean_description(self): # Description should always be empty string, never null. return self.description or '' def validate_unique(self, exclude=None): super(PrimordialModel, self).validate_unique(exclude=exclude) model = type(self) if not hasattr(model, 'SOFT_UNIQUE_TOGETHER'): return errors = [] for ut in model.SOFT_UNIQUE_TOGETHER: kwargs = {} for field_name in ut: kwargs[field_name] = getattr(self, field_name, None) try: obj = model.objects.get(**kwargs) except ObjectDoesNotExist: continue if not (self.pk and self.pk == obj.pk): errors.append('%s with this (%s) combination already exists.' % (model.__name__, ', '.join(set(ut) - {'polymorphic_ctype'}))) if errors: raise ValidationError(errors)
class Version(models.Model): project = models.ForeignKey(Project, verbose_name=_('Project'), related_name='versions') type = models.CharField( _('Type'), max_length=20, choices=VERSION_TYPES, default='unknown', ) # used by the vcs backend identifier = models.CharField(_('Identifier'), max_length=255) verbose_name = models.CharField(_('Verbose Name'), max_length=255) slug = models.CharField(_('Slug'), max_length=255) supported = models.BooleanField(_('Supported'), default=True) active = models.BooleanField(_('Active'), default=False) built = models.BooleanField(_('Built'), default=False) uploaded = models.BooleanField(_('Uploaded'), default=False) privacy_level = models.CharField( _('Privacy Level'), max_length=20, choices=constants.PRIVACY_CHOICES, default=DEFAULT_VERSION_PRIVACY_LEVEL, help_text=_("Level of privacy for this Version.")) tags = TaggableManager(blank=True) machine = models.BooleanField(_('Machine Created'), default=False) objects = VersionManager() class Meta: unique_together = [('project', 'slug')] ordering = ['-verbose_name'] permissions = ( # Translators: Permission around whether a user can view the # version ('view_version', _('View Version')), ) def __unicode__(self): return ugettext(u"Version %(version)s of %(project)s (%(pk)s)" % { 'version': self.verbose_name, 'project': self.project, 'pk': self.pk }) def get_absolute_url(self): if not self.built and not self.uploaded: return '' return self.project.get_docs_url(version_slug=self.slug) def save(self, *args, **kwargs): """ Add permissions to the Version for all owners on save. """ obj = super(Version, self).save(*args, **kwargs) for owner in self.project.users.all(): assign('view_version', owner, self) self.project.sync_supported_versions() return obj @property def remote_slug(self): if self.slug == 'latest': if self.project.default_branch: return self.project.default_branch else: return self.project.vcs_repo().fallback_branch else: return self.slug def get_subdomain_url(self): use_subdomain = getattr(settings, 'USE_SUBDOMAIN', False) if use_subdomain: return "/%s/%s/" % ( self.project.language, self.slug, ) else: return reverse('docs_detail', kwargs={ 'project_slug': self.project.slug, 'lang_slug': self.project.language, 'version_slug': self.slug, 'filename': '' }) def get_subproject_url(self): return "/projects/%s/%s/%s/" % ( self.project.slug, self.project.language, self.slug, ) def get_downloads(self, pretty=False): project = self.project data = {} if pretty: if project.has_pdf(self.slug): data['PDF'] = project.get_production_media_url( 'pdf', self.slug) if project.has_htmlzip(self.slug): data['HTML'] = project.get_production_media_url( 'htmlzip', self.slug) if project.has_epub(self.slug): data['Epub'] = project.get_production_media_url( 'epub', self.slug) else: if project.has_pdf(self.slug): data['pdf_url'] = project.get_production_media_url( 'pdf', self.slug) if project.has_htmlzip(self.slug): data['htmlzip_url'] = project.get_production_media_url( 'htmlzip', self.slug) if project.has_epub(self.slug): data['epub_url'] = project.get_production_media_url( 'epub', self.slug) return data def get_conf_py_path(self): # Hack this for now. return "/docs/" conf_py_path = self.project.conf_file(self.slug) conf_py_path = conf_py_path.replace( self.project.checkout_path(self.slug), '') return conf_py_path.replace('conf.py', '') def get_build_path(self): '''Return version build path if path exists, otherwise `None`''' path = self.project.checkout_path(version=self.slug) if os.path.exists(path): return path return None def get_github_url(self, docroot, filename, source_suffix='.rst', action='view'): GITHUB_REGEXS = [ re.compile('github.com/(.+)/(.+)(?:\.git){1}'), re.compile('github.com/(.+)/(.+)'), re.compile('github.com:(.+)/(.+).git'), ] GITHUB_URL = 'https://github.com/{user}/{repo}/{action}/{version}{docroot}{path}{source_suffix}' repo_url = self.project.repo if 'github' not in repo_url: return '' if not docroot: return '' else: if docroot[0] != '/': docroot = "/%s" % docroot if docroot[-1] != '/': docroot = "%s/" % docroot if action == 'view': action_string = 'blob' elif action == 'edit': action_string = 'edit' for regex in GITHUB_REGEXS: match = regex.search(repo_url) if match: user, repo = match.groups() break else: return '' repo = repo.rstrip('/') return GITHUB_URL.format( user=user, repo=repo, version=self.remote_slug, docroot=docroot, path=filename, source_suffix=source_suffix, action=action_string, ) def get_bitbucket_url(self, docroot, filename, source_suffix='.rst'): BB_REGEXS = [ re.compile('bitbucket.org/(.+)/(.+).git'), re.compile('bitbucket.org/(.+)/(.+)/'), re.compile('bitbucket.org/(.+)/(.+)'), ] BB_URL = 'https://bitbucket.org/{user}/{repo}/src/{version}{docroot}{path}{source_suffix}' repo_url = self.project.repo if 'bitbucket' not in repo_url: return '' if not docroot: return '' for regex in BB_REGEXS: match = regex.search(repo_url) if match: user, repo = match.groups() break else: return '' repo = repo.rstrip('/') return BB_URL.format( user=user, repo=repo, version=self.remote_slug, docroot=docroot, path=filename, source_suffix=source_suffix, )
class Publication(AbstractPublication, CreatedModifiedBy, AbstractLockable): slug = models.SlugField(default='', blank=True, max_length=256) tags = TaggableManager() relevance = models.IntegerField(default=0, blank=True) class Meta: verbose_name_plural = _("Publications") def serialize(self): return { 'slug': self.slug, 'image': self.image_url(), 'name': self.name, 'title': self.title, # 'published': self.date_start, 'subheading': self.subheading, 'categories': (i.name for i in self.categories), 'tags': [i.name for i in self.tags.all()], 'published_in': (f'{i.webpath.site}{i.webpath.fullpath}' for i in self.publicationcontext_set.all()) } def active_translations(self): return PublicationLocalization.objects.filter(publication=self, is_active=True) def image_url(self): if self.preview_image: return self.preview_image.get_media_path() elif self.presentation_image: return self.presentation_image.get_media_path() else: # pragma: no cover categories = self.category.all() for category in categories: if category.image: return sanitize_path( f'{settings.MEDIA_URL}/{category.image}') def image_title(self): # pragma: no cover if self.preview_image: return self.preview_image.title if self.presentation_image: return self.presentation_image.title return self.title def image_description(self): # pragma: no cover if self.preview_image: return self.preview_image.description if self.presentation_image: return self.presentation_image.description return self.subheading @property def categories(self): return self.category.all() @property def related_publications(self): related = PublicationRelated.objects.filter(publication=self, related__is_active=True) # return [i for i in related if i.related.is_publicable] return [i for i in related] @property def related_contexts(self, unique_webpath=True, published=True): contexts = PublicationContext.objects.select_related('webpath')\ .filter(publication=self, is_active=True, webpath__is_active=True) if published: now = timezone.localtime() contexts = contexts.filter(date_start__lte=now, date_end__gte=now) if not unique_webpath: return contexts webpaths = [] unique_contexts = [] for context in contexts: if context.webpath in webpaths: continue webpaths.append(context.webpath) unique_contexts.append(context) return unique_contexts @property def first_available_url(self): now = timezone.localtime() pubcontx = PublicationContext.objects.filter(publication=self, is_active=True, webpath__is_active=True, date_start__lte=now, date_end__gte=now) if pubcontx.exists(): return pubcontx.first().url @property def related_links(self): return self.publicationlink_set.all() @property def related_embedded_links(self): return self.publicationlink_set.all().filter(embedded=True) @property def related_plain_links(self): return self.publicationlink_set.all().filter(embedded=False) @property def related_media_collections(self): if getattr(self, '_related_media_collections', None): return self._related_media_collections pub_collections = PublicationMediaCollection.objects.filter( publication=self, is_active=True, collection__is_active=True) self._related_media_collections = pub_collections return self._related_media_collections def translate_as(self, lang): """ returns translation if available """ trans = PublicationLocalization.objects.filter(publication=self, language=lang, is_active=True).first() if trans: self.title = trans.title self.subheading = trans.subheading self.content = trans.content @property def available_in_languages(self) -> list: return [(i, i.get_language_display()) for i in PublicationLocalization.objects.filter( publication=self, is_active=True)] def title2slug(self): return slugify(self.title) def content_save_switch(self): old_content_type = None if self.pk: current_entry = self.__class__.objects.filter(pk=self.pk).first() if current_entry: old_content_type = current_entry.content_type if all((old_content_type, self.content, self.pk, self.content_type != old_content_type)): # markdown to html if old_content_type == 'html': self.content = markdownify(self.content) elif old_content_type == 'markdown': self.content = markdown(self.content) def save(self, *args, **kwargs): if not self.slug: self.slug = self.title2slug() self.content_save_switch() super(self.__class__, self).save(*args, **kwargs) @property def get_attachments(self): return PublicationAttachment.objects.filter(publication=self, is_active=True).\ order_by('order') @property def get_embedded_attachments(self): return self.get_attachments.filter(embedded=True) @property def get_plain_attachments(self): return self.get_attachments.filter(embedded=False) def get_publication_contexts(self, webpath=None): qdict = dict(publication=self, is_active=True) if webpath: qdict['webpath'] = webpath pub_context = PublicationContext.objects.filter(**qdict) return pub_context def get_publication_context(self, webpath=None): return self.get_publication_contexts(webpath=webpath).first() def url(self, webpath=None): pub_context = self.get_publication_context(webpath=webpath) if not pub_context: return '' return pub_context.url def get_url_list(self, webpath=None, category_name=None): pub_context = self.get_publication_context(webpath=webpath) if not pub_context: return '' return pub_context.get_url_list(category_name=category_name) @property def html_content(self): content = '' if self.content_type == 'markdown': content = markdown(self.content) elif self.content_type == 'html': content = self.content return content def is_localizable_by(self, user=None): if not user: return False # check if user has Django permissions to change object permission = check_user_permission_on_object(user, self) # if permission if permission['granted']: return True # if no permissions and no locks if not permission.get('locked', False): # check if user has EditorialBoard translator permissions on object pub_ctxs = self.get_publication_contexts() for pub_ctx in pub_ctxs: webpath = pub_ctx.webpath webpath_perms = webpath.is_localizable_by(user=user) if webpath_perms: return True # if no permissions return False def is_editable_by(self, user=None): if not user: return False # check if user has Django permissions to change object permission = check_user_permission_on_object(user, self) # if permission if permission['granted']: return True # if no permissions and no locks if not permission.get('locked', False): # check if user has EditorialBoard editor permissions on object pub_ctxs = self.get_publication_contexts() for pub_ctx in pub_ctxs: webpath = pub_ctx.webpath webpath_perms = webpath.is_editable_by(user=user, obj=self) if webpath_perms: return True # if no permissions return False @property def is_publicable(self) -> bool: return self.is_active def is_publicable_by(self, user=None): if not user: return False # check if user has Django permissions to change object permission = check_user_permission_on_object(user, self) # if permission if permission['granted']: return True # if no permissions and no locks if not permission.get('locked', False): # check if user has EditorialBoard editor permissions on object pub_ctxs = self.get_publication_contexts() for pub_ctx in pub_ctxs: webpath = pub_ctx.webpath webpath_perms = webpath.is_publicable_by(user=user, obj=self) if webpath_perms: return True # if no permissions return False def is_lockable_by(self, user): return True if self.is_editable_by(user) else False def __str__(self): return f'{self.name} ({self.title})'
class Post(ModelMeta, models.Model): STATUS_CHOISES = ( ('draft', 'Draft'), ('published', 'Published'), ) title = models.CharField(max_length=250) slug = models.SlugField(allow_unicode=True) #TODO : study about on_delete from doc author = models.ForeignKey(User, related_name='blog_posts', on_delete=models.CASCADE) body = models.TextField() thumbnailImage = FilerImageField(on_delete=models.CASCADE, related_name="blog_thumb_image") likes = models.ManyToManyField(User, related_name='likes', blank=True) tags = TaggableManager() categories = TreeManyToManyField(Categories, related_name="categories") publish = models.DateTimeField(default=timezone.now) created = models.DateTimeField(auto_now_add=True) updated = models.DateTimeField(auto_now=True) status = models.CharField(max_length=10, choices=STATUS_CHOISES, default="draft") class Meta: ordering = ('-publish', ) def __str__(self): return self.title def total_likes(self): return self.likes.count() def blog_views_count(self): return self.views.count() def cat_indexing(self): """«category for indexing. Used in Elasticsearch indexing. """ data = json.dumps([cat.name for cat in self.categories.all()], ensure_ascii=False) jsondata = json.loads(data) return jsondata title_suggest = [] def indexing(self): obj = BlogIndex(meta={'index': 'blog'}, title=self.title, title_suggest=[{ "input": [self.title], "weight": 34 }, { "input": self.cat_indexing(), }, { "input": [tag.name for tag in self.tags.all()], }], text=self.body, cat=self.cat_indexing(), publish=self.publish, author=self.author.username, thumbnailImage=self.thumbnailImage.url, views=self.blog_views_count(), url=self.get_absolute_url()) es = Elasticsearch(['http://elasticsearch613:9200/']) obj.save(es, request_timeout=80) return obj.to_dict(include_meta=True) def get_absolute_url(self): cat = [] for x in self.categories.all(): cat = x.name return reverse('blog:post_detail', args=[cat, self.slug]) def save(self, *args, **kwargs): if self.slug in (None, '', u''): self.slug = slugify(self.title) super(Post, self).save(*args, **kwargs) # Keywords for django seo _metadata = { 'title': 'title', 'description': 'content', 'keywords': 'tag_indexing' }
class Project(models.Model): #Auto fields pub_date = models.DateTimeField(_('Publication date'), auto_now_add=True) modified_date = models.DateTimeField(_('Modified date'), auto_now=True) #Generally from conf.py users = models.ManyToManyField(User, verbose_name=_('User'), related_name='projects') name = models.CharField(_('Name'), max_length=255) slug = models.SlugField(_('Slug'), max_length=255, unique=True) description = models.TextField(_('Description'), blank=True, help_text=_('The reStructuredText ' 'description of the project')) repo = models.CharField(_('Repository URL'), max_length=100, blank=True, help_text=_('Checkout URL for your code (hg, git, ' 'etc.). Ex. http://github.com/' 'ericholscher/django-kong.git')) repo_type = models.CharField(_('Repository type'), max_length=10, choices=constants.REPO_CHOICES, default='git') project_url = models.URLField(_('Project URL'), blank=True, help_text=_('The project\'s homepage'), verify_exists=False) canonical_url = models.URLField(_('Canonical URL'), blank=True, help_text=_('The official URL that the docs live at. This can be at readthedocs.org, or somewhere else. Ex. http://docs.fabfile.org'), verify_exists=False) version = models.CharField(_('Version'), max_length=100, blank=True, help_text=_('Project version these docs apply ' 'to, i.e. 1.0a')) copyright = models.CharField(_('Copyright'), max_length=255, blank=True, help_text=_('Project copyright information')) theme = models.CharField( _('Theme'), max_length=20, choices=constants.DEFAULT_THEME_CHOICES, default=constants.THEME_DEFAULT, help_text=(u'<a href="http://sphinx.pocoo.org/theming.html#builtin-' 'themes" target="_blank">%s</a>') % _('Examples')) suffix = models.CharField(_('Suffix'), max_length=10, editable=False, default='.rst') single_version = models.BooleanField( _('Single version'), default=False, help_text=_('A single version site has no translations and only your "latest" version, served at the root of the domain. Use this with caution, only turn it on if you will <b>never</b> have multiple versions of your docs.')) default_version = models.CharField( _('Default version'), max_length=255, default='latest', help_text=_('The version of your project that / redirects to')) # In default_branch, None max_lengtheans the backend should choose the # appropraite branch. Eg 'master' for git default_branch = models.CharField( _('Default branch'), max_length=255, default=None, null=True, blank=True, help_text=_('What branch "latest" points to. Leave empty ' 'to use the default value for your VCS (eg. ' 'trunk or master).')) requirements_file = models.CharField( _('Requirements file'), max_length=255, default=None, null=True, blank=True, help_text=_( 'Requires Virtualenv. A <a ' 'href="http://www.pip-installer.org/en/latest/cookbook.html#requirements-files">' 'pip requirements file</a> needed to build your documentation. ' 'Path from the root of your project.')) documentation_type = models.CharField( _('Documentation type'), max_length=20, choices=constants.DOCUMENTATION_CHOICES, default='sphinx', help_text=_('Type of documentation you are building. <a href="http://' 'sphinx.pocoo.org/builders.html#sphinx.builders.html.' 'DirectoryHTMLBuilder">More info</a>.')) analytics_code = models.CharField( _('Analytics code'), max_length=50, null=True, blank=True, help_text=_("Google Analytics Tracking ID (ex. UA-22345342-1). " "This may slow down your page loads.")) # Other model data. path = models.CharField(_('Path'), max_length=255, editable=False, help_text=_("The directory where conf.py lives")) conf_py_file = models.CharField( _('Python configuration file'), max_length=255, default='', blank=True, help_text=_('Path from project root to conf.py file (ex. docs/conf.py)' '. Leave blank if you want us to find it for you.')) featured = models.BooleanField(_('Featured')) skip = models.BooleanField(_('Skip')) mirror = models.BooleanField(_('Mirror'), default=False) use_virtualenv = models.BooleanField( _('Use virtualenv'), help_text=_("Install your project inside a virtualenv using setup.py " "install")) # This model attribute holds the python interpreter used to create the # virtual environment python_interpreter = models.CharField( _('Python Interpreter'), max_length=20, choices=constants.PYTHON_CHOICES, default='python', help_text=_("(Beta) The Python interpreter used to create the virtual " "environment.")) use_system_packages = models.BooleanField( _('Use system packages'), help_text=_("Give the virtual environment access to the global " "site-packages dir.")) django_packages_url = models.CharField(_('Django Packages URL'), max_length=255, blank=True) privacy_level = models.CharField( _('Privacy Level'), max_length=20, choices=constants.PRIVACY_CHOICES, default='public', help_text=_("(Beta) Level of privacy that you want on the repository. " "Protected means public but not in listings.")) version_privacy_level = models.CharField( _('Version Privacy Level'), max_length=20, choices=constants.PRIVACY_CHOICES, default='public', help_text=_("(Beta) Default level of privacy you want on built " "versions of documentation.")) # Subprojects related_projects = models.ManyToManyField( 'self', verbose_name=_('Related projects'), blank=True, null=True, symmetrical=False, through=ProjectRelationship) # Language bits language = models.CharField('Language', max_length=20, default='en', help_text="The language the project " "documentation is rendered in. " "Note: this affects your project's URL.", choices=constants.LANGUAGES) # A subproject pointed at it's main language, so it can be tracked main_language_project = models.ForeignKey('self', related_name='translations', blank=True, null=True) # Version State num_major = models.IntegerField( _('Number of Major versions'), max_length=3, default=2, null=True, blank=True, help_text=_("2 means supporting 3.X.X and 2.X.X, but not 1.X.X") ) num_minor = models.IntegerField( _('Number of Minor versions'), max_length=3, default=2, null=True, blank=True, help_text=_("2 means supporting 2.2.X and 2.1.X, but not 2.0.X") ) num_point = models.IntegerField( _('Number of Point versions'), max_length=3, default=2, null=True, blank=True, help_text=_("2 means supporting 2.2.2 and 2.2.1, but not 2.2.0") ) tags = TaggableManager(blank=True) objects = ProjectManager() class Meta: ordering = ('slug',) permissions = ( # Translators: Permission around whether a user can view the # project ('view_project', _('View Project')), ) def __unicode__(self): return self.name @property def subdomain(self): prod_domain = getattr(settings, 'PRODUCTION_DOMAIN') if self.canonical_domain: return self.canonical_domain else: subdomain_slug = self.slug.replace('_', '-') return "%s.%s" % (subdomain_slug, prod_domain) def sync_supported_versions(self): supported = self.supported_versions(flat=True) if supported: self.versions.filter(verbose_name__in=supported).update(supported=True) self.versions.exclude(verbose_name__in=supported).update(supported=False) self.versions.filter(verbose_name='latest').update(supported=True) def save(self, *args, **kwargs): if not self.slug: # Subdomains can't have underscores in them. self.slug = slugify(self.name).replace('_','-') if self.slug == '': raise Exception(_("Model must have slug")) obj = super(Project, self).save(*args, **kwargs) for owner in self.users.all(): assign('view_project', owner, self) # Add exceptions here for safety try: self.sync_supported_versions() except Exception, e: log.error('failed to sync supported versions', exc_info=True) try: symlink(project=self.slug) except Exception, e: log.error('failed to symlink project', exc_info=True)
class Resource(ClusterableModel): title = models.CharField(max_length=255) desc = RichTextField(verbose_name='Description', blank=True) thumbnail = models.ForeignKey('v1.CFGOVImage', null=True, blank=True, on_delete=models.SET_NULL, related_name='+') related_file = models.ForeignKey('wagtaildocs.Document', null=True, blank=True, on_delete=models.SET_NULL, related_name='+') alternate_file = models.ForeignKey('wagtaildocs.Document', null=True, blank=True, on_delete=models.SET_NULL, related_name='+') link = models.URLField( blank=True, help_text='Example: URL to order a few copies of a printed piece.', validators=[URLValidator]) alternate_link = models.URLField( blank=True, help_text='Example: a URL to for ordering bulk copies.', validators=[URLValidator]) order = models.PositiveSmallIntegerField( null=True, blank=True, help_text='Resources will be listed alphabetically by title in a ' 'Resource List module, unless any in the list have a number in this ' 'field; those with an order value will appear in ascending order.') tags = TaggableManager( through=ResourceTag, blank=True, help_text='Tags can be used to filter resources in a Resource List.') objects = TaggableSnippetManager() panels = [ FieldPanel('title'), FieldPanel('desc'), ImageChooserPanel('thumbnail'), DocumentChooserPanel('related_file'), DocumentChooserPanel('alternate_file'), FieldPanel('link'), FieldPanel('alternate_link'), FieldPanel('order'), FieldPanel('tags'), ] # Makes fields available to the Actions chooser in a Resource List module resource_list_field_choices = [ ('related_file', 'Related file'), ('alternate_file', 'Alternate file'), ('link', 'Link'), ('alternate_link', 'Alternate link'), ] def __str__(self): return self.title class Meta: ordering = ( 'order', 'title', )
class AbstractTemplate(ShareableOrgMixin, BaseConfig): """ Abstract model implementing a netjsonconfig template """ tags = TaggableManager( through=get_model_name('config', 'TaggedTemplate'), blank=True, help_text=_( 'A comma-separated list of template tags, may be used ' 'to ease auto configuration with specific settings (eg: ' '4G, mesh, WDS, VPN, ecc.)' ), ) vpn = models.ForeignKey( get_model_name('config', 'Vpn'), verbose_name=_('VPN'), blank=True, null=True, on_delete=models.CASCADE, ) type = models.CharField( _('type'), max_length=16, choices=TYPE_CHOICES, default='generic', db_index=True, help_text=_('template type, determines which features are available'), ) default = models.BooleanField( _('enabled by default'), default=False, db_index=True, help_text=_( 'whether new configurations will have this template enabled by default' ), ) auto_cert = models.BooleanField( _('auto certificate'), default=default_auto_cert, db_index=True, help_text=_( 'whether x509 client certificates should ' 'be automatically managed behind the scenes ' 'for each configuration using this template, ' 'valid only for the VPN type' ), ) default_values = JSONField( _('Default Values'), default=dict, blank=True, help_text=_( 'A dictionary containing the default ' 'values for the variables used by this ' 'template; these default variables will ' 'be used during schema validation.' ), load_kwargs={'object_pairs_hook': OrderedDict}, dump_kwargs={'indent': 4}, ) __template__ = True class Meta: abstract = True verbose_name = _('template') verbose_name_plural = _('templates') unique_together = (('organization', 'name'),) def save(self, *args, **kwargs): """ modifies status of related configs if key attributes have changed (queries the database) """ update_related_config_status = False if not self._state.adding: current = self.__class__.objects.get(pk=self.pk) for attr in ['backend', 'config']: if getattr(self, attr) != getattr(current, attr): update_related_config_status = True break # save current changes super().save(*args, **kwargs) # update relations if update_related_config_status: transaction.on_commit( lambda: update_template_related_config_status.delay(self.pk) ) def _update_related_config_status(self): changing_status = list(self.config_relations.exclude(status='modified')) self.config_relations.update(status='modified') for config in self.config_relations.select_related('device').iterator(): # config modified signal sent regardless config._send_config_modified_signal() # config status changed signal sent only if status changed if config in changing_status: config._send_config_status_changed_signal() def clean(self, *args, **kwargs): """ * validates org relationship of VPN if present * validates default_values field * ensures VPN is selected if type is VPN * clears VPN specific fields if type is not VPN * automatically determines configuration if necessary """ self._validate_org_relation('vpn') if not self.default_values: self.default_values = {} if not isinstance(self.default_values, dict): raise ValidationError( {'default_values': _('the supplied value is not a JSON object')} ) if self.type == 'vpn' and not self.vpn: raise ValidationError( {'vpn': _('A VPN must be selected when template type is "VPN"')} ) elif self.type != 'vpn': self.vpn = None self.auto_cert = False if self.type == 'vpn' and not self.config: self.config = self.vpn.auto_client(auto_cert=self.auto_cert) super().clean(*args, **kwargs) if not self.config: raise ValidationError(_('The configuration field cannot be empty.')) def get_context(self, system=False): context = {} if self.default_values and not system: context = copy(self.default_values) context.update(super().get_context()) return context def get_system_context(self): system_context = self.get_context(system=True) return OrderedDict(sorted(system_context.items())) def clone(self, user): clone = copy(self) clone.name = self.__get_clone_name() clone._state.adding = True clone.pk = None # avoid cloned templates to be flagged as default # to avoid potential unwanted duplications in # newly registrated devices clone.default = False clone.full_clean() clone.save() ct = ContentType.objects.get(model='template') LogEntry.objects.log_action( user_id=user.id, content_type_id=ct.pk, object_id=clone.pk, object_repr=clone.name, action_flag=ADDITION, ) return clone def __get_clone_name(self): name = '{} (Clone)'.format(self.name) index = 2 while self.__class__.objects.filter(name=name).count(): name = '{} (Clone {})'.format(self.name, index) index += 1 return name
class ArticlePost(models.Model): """ 文章的 Model """ # 定义文章作者。 author 通过 models.ForeignKey 外键与内建的 User 模型关联在一起 # 参数 on_delete 用于指定数据删除的方式,避免两个关联表的数据不一致。通常设置为 CASCADE 级联删除就可以了 author = models.ForeignKey(User, on_delete=models.CASCADE) # 文章标题图 avatar = ProcessedImageField( upload_to='article/%Y%m%d', processors=[ResizeToFit(400, 300)], format='JPEG', options={'quality': 100}, ) # 文章栏目的 “一对多” 外键 column = models.ForeignKey(ArticleColumn, null=True, blank=True, on_delete=models.CASCADE, related_name='article') # 文章标签 # 采用 Django-taggit 库 tags = TaggableManager(blank=True) # 文章标题。 # models.CharField 为字符串字段,用于保存较短的字符串,比如标题 # CharField 有一个必填参数 max_length,它规定字符的最大长度 title = models.CharField(max_length=100) # 文章正文。 # 保存大量文本使用 TextField body = models.TextField() # 浏览量 total_views = models.PositiveIntegerField(default=0) # 文章点赞数 likes = models.PositiveIntegerField(default=0) # 文章创建时间。 # DateTimeField 为一个日期字段 # 参数 default=timezone.now 指定其在创建数据时将默认写入当前的时间 created = models.DateTimeField(default=timezone.now) # 文章更新时间。 # 参数 auto_now=True 指定每次数据更新时自动写入当前时间 updated = models.DateTimeField(auto_now=True) # 内部类 class Meta 用于给 model 定义元数据 # 元数据:不是一个字段的任何数据 class Meta: # ordering 指定模型返回的数据的排列顺序 # '-created' 表明数据应该以倒序排列 ordering = ('-created', ) # 函数 __str__ 定义当调用对象的 str() 方法时的返回值内容 # 它最常见的就是在Django管理后台中做为对象的显示值。因此应该总是为 __str__ 返回一个友好易读的字符串 def __str__(self): # 将文章标题返回 return self.title # 获取文章地址 def get_absolute_url(self): return reverse('article:article_detail', args=[self.id]) # 保存时处理图片 # def save(self, *args, **kwargs): # # 调用原有的 save() 的功能 # article = super(ArticlePost, self).save(*args, **kwargs) # # # 固定宽度缩放图片大小 # if self.avatar and not kwargs.get('update_fields'): # image = Image.open(self.avatar) # (x, y) = image.size # new_x = 400 # new_y = int(new_x * (y / x)) # resized_image = image.resize((new_x, new_y), Image.ANTIALIAS) # resized_image.save(self.avatar.path) # # return article def was_created_recently(self): # 若文章是 1 分钟内发表的,则返回 True diff = timezone.now() - self.created # if diff.days <= 0 and diff.seconds < 60: if diff.days == 0 and diff.seconds >= 0 and diff.seconds < 60: return True else: return False
class Manifest(models.Model): """ Class to represent Manifest of dir. objects """ name = models.CharField("name", max_length=255) caption = models.CharField("Caption", max_length=255, null=True, blank=True) subject = models.CharField("Subject", max_length=255, default='', null=True, blank=True) directory = models.ForeignKey(Directory, related_name='core_manifest_dir', on_delete=models.DO_NOTHING) location = models.ForeignKey(Location, related_name='core_manifest_loc', on_delete=models.DO_NOTHING, default=1) section = models.ForeignKey(Section, related_name='core_manifest_section', on_delete=models.DO_NOTHING, default=1) company = models.ForeignKey(Company, related_name='core_manifest_company', on_delete=models.DO_NOTHING, default=1) sequence = models.CharField("Sequence", max_length=255) thumb_path = models.CharField("Thumb Path", max_length=500) src_path = models.CharField("Src Path", max_length=500) import_status = models.CharField("Import Status", max_length=255, default='init' ) date = models.DateField("Date") exif_date = models.DateTimeField("Date Time", null=True, blank=True) lat = models.FloatField("lat", default=0) lng = models.FloatField("lng", default=0) meta_1 = models.CharField("meta_1", max_length=255, default="", null=True, blank=True ) meta_2 = models.CharField("meta_2", max_length=255, default="", null=True, blank=True ) meta_3 = models.CharField("meta_3", max_length=255, default="", null=True, blank=True ) tags = TaggableManager() exif_data = models.TextField("exif", null=True, blank=True) created_at = models.DateTimeField("Created at", auto_now_add=True) updated_at = models.DateTimeField("Updated at", auto_now=True) def __str__(self): return str(self.name) def _format(req, row, status): data = {'name': row.get('name'), 'subject': req.get('subject', ''), 'caption': req.get('caption', ''), 'directory_id': req.get('directory_id'), 'company_id': req.get('company_id', 2), 'location_id': req.get('location_id'), 'section_id': req.get('section_id'), 'src_path':"/static/{}/{}".format(req.get('dir'), row.get('name') ), 'thumb_path': "/static/{}/thumbs/{}".format(req.get('dir'), row.get('name')), 'sequence': req.get('seq',0), 'import_status': status, } exif = Manifest.get_exif(row['exif'].items(), ['DateTime', 'DateTimeOriginal', 'latlng']) try: data['exif_data'] = json.dumps(exif) except Excption as e: pass date = Manifest.get_date(exif, get_date_from_ts(row.get('file_date')) ) data['date'] = format_date(date) data['exif_date'] = get_date_from_ts(row.get('file_date')) lat, lng = Manifest.get_lat_lng(exif.get('latlng', [])) data['lat'] = lat data['lng'] = lng return data def get_exif(exif, extract=[]): meta = {} for k, v in exif: if k not in extract: continue if type(v) == bytes: v = v.decode("utf8", errors='ignore') meta[k] = str(v) return meta def get_lat_lng(lat_lng): if lat_lng and len(lat_lng) > 0: try: return [float(lat_lng[0]), float(lat_lng[1]) ] except Excption as e: pass return [0, 0] def get_date(exif, default_date): if 'DateTime' not in exif: return default_date d = default_date if exif['DateTime'] == None else exif['DateTime']; return d def default_fields(): return ( 'id', 'name', 'caption', 'subject', 'directory_id', 'directory__name', 'date', 'exif_date', "import_status", 'company_id', 'company__name', 'lat', 'lng', 'location_id', 'location__name', 'section_id', 'section__name', 'sequence', 'src_path', 'thumb_path', 'created_at', 'updated_at' ) class Meta: pass
class AbstractImage(ImageFileMixin, CollectionMember, index.Indexed, models.Model): title = models.CharField(max_length=255, verbose_name=_('title')) file = models.ImageField(verbose_name=_('file'), upload_to=get_upload_to, width_field='width', height_field='height') width = models.IntegerField(verbose_name=_('width'), editable=False) height = models.IntegerField(verbose_name=_('height'), editable=False) created_at = models.DateTimeField(verbose_name=_('created at'), auto_now_add=True, db_index=True) uploaded_by_user = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name=_('uploaded by user'), null=True, blank=True, editable=False, on_delete=models.SET_NULL) tags = TaggableManager(help_text=None, blank=True, verbose_name=_('tags')) focal_point_x = models.PositiveIntegerField(null=True, blank=True) focal_point_y = models.PositiveIntegerField(null=True, blank=True) focal_point_width = models.PositiveIntegerField(null=True, blank=True) focal_point_height = models.PositiveIntegerField(null=True, blank=True) file_size = models.PositiveIntegerField(null=True, editable=False) # A SHA-1 hash of the file contents file_hash = models.CharField(max_length=40, blank=True, editable=False) objects = ImageQuerySet.as_manager() def _set_file_hash(self, file_contents): self.file_hash = hashlib.sha1(file_contents).hexdigest() def get_file_hash(self): if self.file_hash == '': with self.open_file() as f: self._set_file_hash(f.read()) self.save(update_fields=['file_hash']) return self.file_hash def get_upload_to(self, filename): folder_name = 'original_images' filename = self.file.field.storage.get_valid_name(filename) # do a unidecode in the filename and then # replace non-ascii characters in filename with _ , to sidestep issues with filesystem encoding filename = "".join( (i if ord(i) < 128 else '_') for i in string_to_ascii(filename)) # Truncate filename so it fits in the 100 character limit # https://code.djangoproject.com/ticket/9893 full_path = os.path.join(folder_name, filename) if len(full_path) >= 95: chars_to_trim = len(full_path) - 94 prefix, extension = os.path.splitext(filename) filename = prefix[:-chars_to_trim] + extension full_path = os.path.join(folder_name, filename) return full_path def get_usage(self): return get_object_usage(self) @property def usage_url(self): return reverse('wagtailimages:image_usage', args=(self.id, )) search_fields = CollectionMember.search_fields + [ index.SearchField('title', partial_match=True, boost=10), index.AutocompleteField('title'), index.FilterField('title'), index.RelatedFields('tags', [ index.SearchField('name', partial_match=True, boost=10), index.AutocompleteField('name'), ]), index.FilterField('uploaded_by_user'), ] def __str__(self): return self.title def get_rect(self): return Rect(0, 0, self.width, self.height) def get_focal_point(self): if self.focal_point_x is not None and \ self.focal_point_y is not None and \ self.focal_point_width is not None and \ self.focal_point_height is not None: return Rect.from_point( self.focal_point_x, self.focal_point_y, self.focal_point_width, self.focal_point_height, ) def has_focal_point(self): return self.get_focal_point() is not None def set_focal_point(self, rect): if rect is not None: self.focal_point_x = rect.centroid_x self.focal_point_y = rect.centroid_y self.focal_point_width = rect.width self.focal_point_height = rect.height else: self.focal_point_x = None self.focal_point_y = None self.focal_point_width = None self.focal_point_height = None def get_suggested_focal_point(self): with self.get_willow_image() as willow: faces = willow.detect_faces() if faces: # Create a bounding box around all faces left = min(face[0] for face in faces) top = min(face[1] for face in faces) right = max(face[2] for face in faces) bottom = max(face[3] for face in faces) focal_point = Rect(left, top, right, bottom) else: features = willow.detect_features() if features: # Create a bounding box around all features left = min(feature[0] for feature in features) top = min(feature[1] for feature in features) right = max(feature[0] for feature in features) bottom = max(feature[1] for feature in features) focal_point = Rect(left, top, right, bottom) else: return None # Add 20% to width and height and give it a minimum size x, y = focal_point.centroid width, height = focal_point.size width *= 1.20 height *= 1.20 width = max(width, 100) height = max(height, 100) return Rect.from_point(x, y, width, height) @classmethod def get_rendition_model(cls): """ Get the Rendition model for this Image model """ return cls.renditions.rel.related_model def get_rendition(self, filter): if isinstance(filter, str): filter = Filter(spec=filter) cache_key = filter.get_cache_key(self) Rendition = self.get_rendition_model() try: rendition_caching = True cache = caches['renditions'] rendition_cache_key = Rendition.construct_cache_key( self.id, cache_key, filter.spec) cached_rendition = cache.get(rendition_cache_key) if cached_rendition: return cached_rendition except InvalidCacheBackendError: rendition_caching = False try: rendition = self.renditions.get( filter_spec=filter.spec, focal_point_key=cache_key, ) except Rendition.DoesNotExist: # Generate the rendition image try: logger.debug( "Generating '%s' rendition for image %d", filter.spec, self.pk, ) start_time = time.time() generated_image = filter.run(self, BytesIO()) logger.debug("Generated '%s' rendition for image %d in %.1fms", filter.spec, self.pk, (time.time() - start_time) * 1000) except: # noqa:B901,E722 logger.debug("Failed to generate '%s' rendition for image %d", filter.spec, self.pk) raise # Generate filename input_filename = os.path.basename(self.file.name) input_filename_without_extension, input_extension = os.path.splitext( input_filename) # A mapping of image formats to extensions FORMAT_EXTENSIONS = { 'jpeg': '.jpg', 'png': '.png', 'gif': '.gif', 'webp': '.webp', } output_extension = filter.spec.replace( '|', '.') + FORMAT_EXTENSIONS[generated_image.format_name] if cache_key: output_extension = cache_key + '.' + output_extension # Truncate filename to prevent it going over 60 chars output_filename_without_extension = input_filename_without_extension[:( 59 - len(output_extension))] output_filename = output_filename_without_extension + '.' + output_extension rendition, created = self.renditions.get_or_create( filter_spec=filter.spec, focal_point_key=cache_key, defaults={ 'file': File(generated_image.f, name=output_filename) }) if rendition_caching: cache.set(rendition_cache_key, rendition) return rendition def is_portrait(self): return (self.width < self.height) def is_landscape(self): return (self.height < self.width) @property def filename(self): return os.path.basename(self.file.name) @property def default_alt_text(self): # by default the alt text field (used in rich text insertion) is populated # from the title. Subclasses might provide a separate alt field, and # override this return self.title def is_editable_by_user(self, user): from wagtail.images.permissions import permission_policy return permission_policy.user_has_permission_for_instance( user, 'change', self) class Meta: abstract = True
class Person(models.Model): '''The main class of the model. Every individual is represented by a person record.''' forename = models.CharField(max_length=20, help_text='Forename / given name') middle_names = models.CharField(blank=True, max_length=50, help_text='Middle names(s)') known_as = models.CharField(blank=True, max_length=20, help_text='Known as') surname = models.CharField(max_length=30, help_text='Surname') maiden_name = models.CharField( blank=True, max_length=30, help_text='Maiden name') # Maiden name is optional. gender = models.CharField(max_length=1, choices=(('M', 'Male'), ('F', 'Female')), blank=False, default=None) birth = models.ForeignKey('Event', models.SET_NULL, null=True, blank=True, related_name='+') death = models.ForeignKey('Event', models.SET_NULL, null=True, blank=True, related_name='+') deceased = models.BooleanField(default=True) blood_relative = models.BooleanField(default=True) mother = models.ForeignKey('self', blank=True, null=True, limit_choices_to={'gender': 'F'}, related_name='children_of_mother', on_delete=models.SET_NULL) father = models.ForeignKey('self', blank=True, null=True, limit_choices_to={'gender': 'M'}, related_name='children_of_father', on_delete=models.SET_NULL) notes = HTMLField(blank=True) tags = TaggableManager(blank=True, help_text='Tags') # A person can be linked to a user account. This allows a user to see # information relevant to their own relationships. user = models.OneToOneField(User, blank=True, null=True) avatar = models.ImageField(upload_to=renameUploadedFile('avatar'), null=True, blank=True, help_text='Avatar Image') def name(self, use_middle_names=True, use_maiden_name=False): '''Returns the full name of this person.''' name = ' '.join([ self.forename, self.middle_names ]) if use_middle_names and self.middle_names else self.forename if self.known_as: name = name + ' "{0}"'.format(self.known_as) if self.maiden_name != '': return name + ' ' + (self.maiden_name if use_maiden_name else self.surname.upper() + u' (n\xe9e ' + self.maiden_name + ')') else: return name + ' ' + self.surname.upper() def given_names(self): return " ".join([self.forename, self.middle_names ]) if self.middle_names else self.forename def birth_surname(self): return self.maiden_name if self.maiden_name else self.surname def birth_name(self): return '{0} {1}'.format(self.given_names(), self.birth_surname().upper()) def date_of_birth(self): return self.birth.date if self.birth else None def birth_location(self): return self.birth.location if self.birth else None def date_of_death(self): return self.death.date if self.death else None def age(self): '''Calculate the person's age in years.''' if not self.date_of_birth() or (self.deceased and not self.date_of_death()): return None end = self.date_of_death() if self.deceased else date.today() years = end.year - self.date_of_birth().year if end.month and self.date_of_birth().month: if end.month < self.date_of_birth().month \ or (end.month == self.date_of_birth().month \ and end.day and self.date_of_birth().day and end.day < self.date_of_birth().day): years -= 1 return years def year_range(self): return '{0}-{1}'.format( self.date_of_birth().year if self.date_of_birth() else '????', '' if not self.deceased else self.date_of_death().year if self.date_of_death() else '????') def spouses(self): '''Return a list of anybody that this person is or was married to.''' if self.gender == 'F': return [(m.husband, m.date, m.location, m.divorced) for m in self.wife_of.all()] else: return [(m.wife, m.date, m.location, m.divorced) for m in self.husband_of.all()] def siblings(self): '''Returns a list of this person's brothers and sisters, including half-siblings.''' return Person.objects.filter(~Q(id=self.id), Q(~Q(father=None), father=self.father) | \ Q(~Q(mother=None), mother=self.mother)).order_by('birth__date') def children(self): '''Returns a list of this person's children.''' offspring = self.children_of_mother if self.gender == 'F' else self.children_of_father return offspring.select_related('birth', 'death').order_by('birth__date') def marriages(self): return self.husband_of.all( ) if self.gender == 'M' else self.wife_of.all() def timeline(self): timeline = list(self.events.all()) + list( self.marriages().filter(date__isnull=False)) timeline.sort(key=attrgetter('date')) timeline.sort(key=attrgetter('event_type')) return timeline def _descendant_distances(self, offset=0): descendants = {} for child in self.children(): descendants[child] = offset + 1 descendants.update(child._descendant_distances(offset + 1)) return descendants def descendants(self): '''Returns a list of this person's descendants (their children and all of their children's descendants).''' for child in self.children(): yield child yield from child.descendants() def annotated_descendants(self): '''Returns a list of this person's descendants annotated with the name of the relationship to this person (so a list of (Person, relationship) tuples.''' distances = self._descendant_distances() descendants = [] for descendant in distances.keys(): relationship = describe_relative(self, descendant, {}, descendant._ancestor_distances()) descendants.append( (descendant, relationship, distances[descendant])) descendants.sort(key=lambda x: (x[2], x[1], x[0].surname)) return descendants def annotated_descendants2(self): '''Returns a list of this person's descendants annotated with the name of the relationship to this person and distance. Suitable for d3.js tree charts''' distances = self._descendant_distances() descendants = [] for descendant in distances.keys(): relationship = describe_relative(self, descendant, {}, descendant._ancestor_distances()) descendants.append({ "name": descendant.name(), "avatar": str(descendant.avatar), "mother_id": descendant.mother_id, "father_id": descendant.father_id, "id": descendant.id, "relationship": relationship, "distance": distances[descendant] }) #descendants.sort(key=lambda x: (x[2], x[1], x[0].surname)) return descendants # Returns a dictionary of this person's ancestors. The ancestors are the # keys and each value is the distance (number of generations) from this # person to that ancestor (e.g parent is 1, grandparent is 2, etc.) def _ancestor_distances(self, offset=0): '''Returns a dictionary of this person's ancestors (their parents and all of their parents's ancestors) with distance to each ancestor.''' ancestors = {} if self.mother: ancestors[self.mother] = offset + 1 ancestors.update(self.mother._ancestor_distances(offset + 1)) if self.father: ancestors[self.father] = offset + 1 ancestors.update(self.father._ancestor_distances(offset + 1)) return ancestors def ancestors(self): '''Returns a list of this person's ancestors (their parents and all of their parents' ancestors).''' if self.mother: yield self.mother yield from self.mother.ancestors() if self.father: yield self.father yield from self.father.ancestors() def annotated_ancestors(self): '''Returns a list of this person's ancestors annotated with the name of the relationship to this person and the distance between them (so a list of (Person, relationship, distance) tuples).''' distances = self._ancestor_distances() ancestors = [] for ancestor in distances.keys(): relationship = describe_relative(self, ancestor, distances, {}) ancestors.append((ancestor, relationship, distances[ancestor])) ancestors.sort(key=lambda x: (x[2], x[1], x[0].surname)) return ancestors def relatives(self): relatives = self._build_relatives_set(set()) relatives.discard(self) # This person can't be their own relative. return relatives def _build_relatives_set(self, relatives_set): '''Adds all blood relatives of this person to the specified set.''' relatives_set.add(self) for child in self.children(): if child not in relatives_set: relatives_set.add(child) relatives_set.update(child.descendants()) if self.father: self.father._build_relatives_set(relatives_set) if self.mother: self.mother._build_relatives_set(relatives_set) return relatives_set def annotated_relatives(self): '''Returns a list of all of this person's blood relatives. The first item in each tuple is the person, the second is the relationship, and the third is the distance between the two individuals.''' ancestor_distances = self._ancestor_distances() distances = ancestor_distances.copy() distances.update(self._descendant_distances()) annotated = [] for relative in self.relatives(): distance = distances.get(relative, None) relative_distances = relative._ancestor_distances() if not distance: (_, d1, d2) = closest_common_ancestor(ancestor_distances, relative_distances) distance = max(d1, d2) relationship = describe_relative(self, relative, ancestor_distances, relative_distances) annotated.append((relative, relationship, distance)) annotated.sort(key=lambda x: (x[2], x[1], x[0].surname)) return annotated def photos(self): '''Returns a list of all photos associated with this person.''' return Photograph.objects.filter(person=self) def has_missing_maiden_name(self): return self.gender == 'F' and self.wife_of.count() > 0 and ( self.maiden_name == '' or self.maiden_name == None) def clean(self): if self.date_of_death() and not self.deceased: raise ValidationError( 'Cannot specify date of death for living person.') if self.birth and self.birth.person.id != self.id: raise ValidationError( 'Birth event must refer back to the same person.') if self.death and self.death.person.id != self.id: raise ValidationError( 'Death event must refer back to the same person.') if (self.mother and self.mother == self) or (self.father and self.father == self): raise ValidationError('Person cannot be their own parent.') def get_absolute_url(self): return reverse('person', args=[self.id]) def __str__(self): return self.name() class Meta: ordering = ['surname', 'forename', 'middle_names', '-birth__date']
class Article(CachingMixin, models.Model): created = models.DateTimeField(auto_now_add=True) modified = models.DateTimeField(auto_now=True) is_live = models.BooleanField('Display on site', default=True) show_in_lists = models.BooleanField(default=True) allow_comments = models.BooleanField(default=True) title = models.CharField(max_length=128) slug = models.SlugField(unique=True) pubdate = models.DateTimeField(default=datetime.now) subhead = models.CharField(max_length=128) authors = models.ManyToManyField(Person, blank=True, null=True, related_name='article_authors') image = ImageField(upload_to='img/uploads/article_images', help_text='Resized to fit 100% column width in template', blank=True, null=True) image_caption = models.TextField(blank=True) image_credit = models.CharField(max_length=128, blank=True, help_text='Optional. Will be appended to end of caption in parens. Accepts HTML.') body = models.TextField() summary = models.TextField() article_type = models.CharField(max_length=32, blank=True) category = models.ForeignKey('Category') people = models.ManyToManyField(Person, blank=True, null=True) organizations = models.ManyToManyField(Organization, blank=True, null=True) code = models.ManyToManyField(Code, blank=True, null=True) tags = TaggableManager(blank=True, help_text='Automatic combined list of Technology Tags and Concept Tags, for easy searching') technology_tags = TaggableManager(verbose_name='Technology Tags', help_text='A comma-separated list of tags describing relevant technologies', through=TechnologyTaggedItem, blank=True) concept_tags = TaggableManager(verbose_name='Concept Tags', help_text='A comma-separated list of tags describing relevant concepts', through=ConceptTaggedItem, blank=True) objects = models.Manager() live_objects = LiveArticleManager() disable_auto_linebreaks = models.BooleanField(default=False, help_text='Check this if body and article blocks already have HTML paragraph tags.') class Meta: ordering = ('-pubdate','title',) def __unicode__(self): return u'%s' % self.title @models.permalink def get_absolute_url(self): return ('article_detail', (), { 'section': self.section.slug, 'slug': self.slug }) @property def section(self): '''follow article category through to section''' if self.category: return self.category.section return None @property def pretty_pubdate(self): '''pre-process for simpler template logic''' return dj_date(self.pubdate,"F j, Y") @property def pretty_caption(self): '''pre-process for simpler template logic''' _caption = self.image_caption or '' _credit = self.image_credit if _credit: _caption = '%s (%s)' % (_caption, _credit) return _caption @property def pretty_body_text(self): '''pre-process for simpler template logic''' _body = self.body if not self.disable_auto_linebreaks: # allow admin users to provide text # that already contains <p> tags _body = linebreaks(_body) return _body @property def safe_summary(self): '''suitable for use in places that must avoid nested anchor tags''' return removetags(self.summary, 'a') @property def merged_tag_list(self): '''return a combined list of technology_tags and concept_tags''' return [item for item in itertools.chain(self.technology_tags.all(), self.concept_tags.all())] def get_live_organization_set(self): return self.organizations.filter(is_live=True) def get_live_people_set(self): return self.people.filter(is_live=True) def get_live_author_set(self): author_set = self.authors.filter(is_live=True) return author_set def get_live_code_set(self): return self.code.filter(is_live=True) def get_live_author_bio_set(self): # only authors with acutal bio information author_set = self.get_live_author_set().exclude(description='') # filter out bio boxes for Erin, Erika, authors_to_exclude = ['erin-kissane','erika-owens','kio-stark'] author_set = author_set.exclude(slug__in=authors_to_exclude) return author_set
class ProposalBase(models.Model): objects = InheritanceManager() kind = models.ForeignKey(ProposalKind) title = models.CharField(max_length=100) description = models.TextField( _("Description"), max_length=400, # @@@ need to enforce 400 in UI help_text= "If your talk is accepted this will be made public and printed in the " "program. Should be one paragraph, maximum 400 characters.") abstract = models.TextField( _("Detailed Abstract"), help_text=_("Detailed description. Will be made public " "if your talk is accepted.")) additional_notes = models.TextField( blank=True, help_text=_("Anything else you'd like the program committee to know " "when making their selection: your past speaking " "experience, open source community experience, etc.")) submitted = models.DateTimeField( default=datetime.datetime.now, editable=False, ) speaker = models.ForeignKey("speakers.Speaker", related_name="proposals") additional_speakers = models.ManyToManyField("speakers.Speaker", through="AdditionalSpeaker", blank=True) cancelled = models.BooleanField(default=False) tags = TaggableManager(blank=True) cached_tags = models.TextField(blank=True, default='', editable=False) class Meta: ordering = ['title'] def __unicode__(self): return self.title def can_edit(self): """ Return True if this proposal is editable - meaning no presentation exists yet. """ # Putting this import at the top would result in a circular import from symposion.schedule.models import Presentation return not Presentation.objects.filter(proposal_base=self).exists() def cache_tags(self): self.cached_tags = self.get_tags_display() self.save() def get_tags_display(self): return u", ".join(self.tags.names()) @property def section(self): return self.kind.section @property def speaker_email(self): return self.speaker.email @property def number(self): return str(self.pk).zfill(3) @property def status(self): try: return self.result.status except ObjectDoesNotExist: return 'undecided' def as_dict(self, details=False): """Return a dictionary representation of this proposal.""" # Put together the base dict. answer = { 'id': self.id, 'speakers': [i.as_dict for i in self.speakers()], 'status': self.status, 'title': self.title, } # Include details iff they're requested. if details: answer['details'] = { 'abstract': self.abstract, 'description': self.description, 'notes': self.additional_notes, } # If there is extra data that has been set, include it also. try: answer['extra'] = json.loads(self.data.data) except ObjectDoesNotExist: pass # Return the answer. return answer def speakers(self): yield self.speaker for speaker in self.additional_speakers.exclude( additionalspeaker__status=AdditionalSpeaker. SPEAKING_STATUS_DECLINED): yield speaker def notification_email_context(self): return { "title": self.title, "speaker": self.speaker.name, "speakers": ', '.join([x.name for x in self.speakers()]), "kind": self.kind.name, }
def test_deconstruct_kwargs_kept(self): instance = TaggableManager(through=OfficialThroughModel) name, path, args, kwargs = instance.deconstruct() new_instance = TaggableManager(*args, **kwargs) self.assertEqual(instance.rel.through, new_instance.rel.through)
def test_deconstruct_kwargs_kept(self): instance = TaggableManager(through=OfficialThroughModel, to='dummy.To') name, path, args, kwargs = instance.deconstruct() new_instance = TaggableManager(*args, **kwargs) self.assertEqual('tests.OfficialThroughModel', _remote_field(new_instance).through) self.assertEqual('dummy.To', _related_model(_remote_field(new_instance)))
try: user = User.objects.get(username=row__): except: #User not Found create_user() print "No user with those names" nationality = row__ current_location = row__ work = row__ startup_status = row__ portfolio_status = row__ itc_program_name = row__ itc_program_year = row__ linked_in_url = row__ skills = TaggableManager() skills.add(row__) profile_tuple = (user=user, nationality=nationality, current_location, current_location=current_location, work=work, startup_status=startup_status, portfolio_status=portfolio_status, itc_program_name=itc_program_name, itc_program_year=itc_program_year) UserProfile.objects.create(profile_tuple) return
class Project(models.Model): """Project model""" # Auto fields pub_date = models.DateTimeField(_('Publication date'), auto_now_add=True) modified_date = models.DateTimeField(_('Modified date'), auto_now=True) # Generally from conf.py users = models.ManyToManyField(User, verbose_name=_('User'), related_name='projects') name = models.CharField(_('Name'), max_length=255) slug = models.SlugField(_('Slug'), max_length=255, unique=True) description = models.TextField(_('Description'), blank=True, help_text=_('The reStructuredText ' 'description of the project')) repo = models.CharField(_('Repository URL'), max_length=255, help_text=_('Hosted documentation repository URL')) repo_type = models.CharField(_('Repository type'), max_length=10, choices=constants.REPO_CHOICES, default='git') project_url = models.URLField(_('Project homepage'), blank=True, help_text=_('The project\'s homepage')) canonical_url = models.URLField(_('Canonical URL'), blank=True, help_text=_('URL that documentation is expected to serve from')) version = models.CharField(_('Version'), max_length=100, blank=True, help_text=_('Project version these docs apply ' 'to, i.e. 1.0a')) copyright = models.CharField(_('Copyright'), max_length=255, blank=True, help_text=_('Project copyright information')) theme = models.CharField( _('Theme'), max_length=20, choices=constants.DEFAULT_THEME_CHOICES, default=constants.THEME_DEFAULT, help_text=(u'<a href="http://sphinx.pocoo.org/theming.html#builtin-' 'themes" target="_blank">%s</a>') % _('Examples')) suffix = models.CharField(_('Suffix'), max_length=10, editable=False, default='.rst') single_version = models.BooleanField( _('Single version'), default=False, help_text=_('A single version site has no translations and only your ' '"latest" version, served at the root of the domain. Use ' 'this with caution, only turn it on if you will <b>never</b> ' 'have multiple versions of your docs.')) default_version = models.CharField( _('Default version'), max_length=255, default=LATEST, help_text=_('The version of your project that / redirects to')) # In default_branch, None max_lengtheans the backend should choose the # appropraite branch. Eg 'master' for git default_branch = models.CharField( _('Default branch'), max_length=255, default=None, null=True, blank=True, help_text=_('What branch "latest" points to. Leave empty ' 'to use the default value for your VCS (eg. ' '<code>trunk</code> or <code>master</code>).')) requirements_file = models.CharField( _('Requirements file'), max_length=255, default=None, null=True, blank=True, help_text=_( 'A <a ' 'href="https://pip.pypa.io/en/latest/user_guide.html#requirements-files">' 'pip requirements file</a> needed to build your documentation. ' 'Path from the root of your project.')) documentation_type = models.CharField( _('Documentation type'), max_length=20, choices=constants.DOCUMENTATION_CHOICES, default='sphinx', help_text=_('Type of documentation you are building. <a href="http://' 'sphinx-doc.org/builders.html#sphinx.builders.html.' 'DirectoryHTMLBuilder">More info</a>.')) # Project features allow_comments = models.BooleanField(_('Allow Comments'), default=False) comment_moderation = models.BooleanField(_('Comment Moderation)'), default=False) cdn_enabled = models.BooleanField(_('CDN Enabled'), default=False) analytics_code = models.CharField( _('Analytics code'), max_length=50, null=True, blank=True, help_text=_("Google Analytics Tracking ID " "(ex. <code>UA-22345342-1</code>). " "This may slow down your page loads.")) container_image = models.CharField( _('Alternative container image'), max_length=64, null=True, blank=True) container_mem_limit = models.CharField( _('Container memory limit'), max_length=10, null=True, blank=True, help_text=_("Memory limit in Docker format " "-- example: <code>512m</code> or <code>1g</code>")) container_time_limit = models.CharField( _('Container time limit'), max_length=10, null=True, blank=True) build_queue = models.CharField( _('Alternate build queue id'), max_length=32, null=True, blank=True) allow_promos = models.BooleanField( _('Sponsor advertisements'), default=True, help_text=_( "Allow sponsor advertisements on my project documentation")) # Sphinx specific build options. enable_epub_build = models.BooleanField( _('Enable EPUB build'), default=True, help_text=_( 'Create a EPUB version of your documentation with each build.')) enable_pdf_build = models.BooleanField( _('Enable PDF build'), default=True, help_text=_( 'Create a PDF version of your documentation with each build.')) # Other model data. path = models.CharField(_('Path'), max_length=255, editable=False, help_text=_("The directory where " "<code>conf.py</code> lives")) conf_py_file = models.CharField( _('Python configuration file'), max_length=255, default='', blank=True, help_text=_('Path from project root to <code>conf.py</code> file ' '(ex. <code>docs/conf.py</code>).' 'Leave blank if you want us to find it for you.')) featured = models.BooleanField(_('Featured'), default=False) skip = models.BooleanField(_('Skip'), default=False) mirror = models.BooleanField(_('Mirror'), default=False) install_project = models.BooleanField( _('Install Project'), help_text=_("Install your project inside a virtualenv using <code>setup.py " "install</code>"), default=False ) # This model attribute holds the python interpreter used to create the # virtual environment python_interpreter = models.CharField( _('Python Interpreter'), max_length=20, choices=constants.PYTHON_CHOICES, default='python', help_text=_("(Beta) The Python interpreter used to create the virtual " "environment.")) use_system_packages = models.BooleanField( _('Use system packages'), help_text=_("Give the virtual environment access to the global " "site-packages dir."), default=False ) django_packages_url = models.CharField(_('Django Packages URL'), max_length=255, blank=True) privacy_level = models.CharField( _('Privacy Level'), max_length=20, choices=constants.PRIVACY_CHOICES, default=getattr(settings, 'DEFAULT_PRIVACY_LEVEL', 'public'), help_text=_("(Beta) Level of privacy that you want on the repository. " "Protected means public but not in listings.")) version_privacy_level = models.CharField( _('Version Privacy Level'), max_length=20, choices=constants.PRIVACY_CHOICES, default=getattr( settings, 'DEFAULT_PRIVACY_LEVEL', 'public'), help_text=_("(Beta) Default level of privacy you want on built " "versions of documentation.")) # Subprojects related_projects = models.ManyToManyField( 'self', verbose_name=_('Related projects'), blank=True, symmetrical=False, through=ProjectRelationship) # Language bits language = models.CharField(_('Language'), max_length=20, default='en', help_text=_("The language the project " "documentation is rendered in. " "Note: this affects your project's URL."), choices=constants.LANGUAGES) programming_language = models.CharField( _('Programming Language'), max_length=20, default='words', help_text=_("The primary programming language the project is written in."), choices=constants.PROGRAMMING_LANGUAGES, blank=True) # A subproject pointed at it's main language, so it can be tracked main_language_project = models.ForeignKey('self', related_name='translations', blank=True, null=True) # Version State num_major = models.IntegerField( _('Number of Major versions'), default=2, null=True, blank=True, help_text=_("2 means supporting 3.X.X and 2.X.X, but not 1.X.X") ) num_minor = models.IntegerField( _('Number of Minor versions'), default=2, null=True, blank=True, help_text=_("2 means supporting 2.2.X and 2.1.X, but not 2.0.X") ) num_point = models.IntegerField( _('Number of Point versions'), default=2, null=True, blank=True, help_text=_("2 means supporting 2.2.2 and 2.2.1, but not 2.2.0") ) has_valid_webhook = models.BooleanField( default=False, help_text=_('This project has been built with a webhook') ) has_valid_clone = models.BooleanField( default=False, help_text=_('This project has been successfully cloned') ) tags = TaggableManager(blank=True) objects = ProjectManager() all_objects = models.Manager() class Meta: ordering = ('slug',) permissions = ( # Translators: Permission around whether a user can view the # project ('view_project', _('View Project')), ) def __unicode__(self): return self.name @property def subdomain(self): try: domain = self.domains.get(canonical=True) return domain.domain except (Domain.DoesNotExist, MultipleObjectsReturned): subdomain_slug = self.slug.replace('_', '-') prod_domain = getattr(settings, 'PRODUCTION_DOMAIN') return "%s.%s" % (subdomain_slug, prod_domain) def sync_supported_versions(self): supported = self.supported_versions() if supported: self.versions.filter( verbose_name__in=supported).update(supported=True) self.versions.exclude( verbose_name__in=supported).update(supported=False) self.versions.filter(verbose_name=LATEST_VERBOSE_NAME).update(supported=True) def save(self, *args, **kwargs): from readthedocs.projects import tasks first_save = self.pk is None if not self.slug: # Subdomains can't have underscores in them. self.slug = slugify(self.name).replace('_', '-') if self.slug == '': raise Exception(_("Model must have slug")) super(Project, self).save(*args, **kwargs) for owner in self.users.all(): assign('view_project', owner, self) try: if self.default_branch: latest = self.versions.get(slug=LATEST) if latest.identifier != self.default_branch: latest.identifier = self.default_branch latest.save() except Exception: log.error('Failed to update latest identifier', exc_info=True) # Add exceptions here for safety try: self.sync_supported_versions() except Exception: log.error('failed to sync supported versions', exc_info=True) try: if not first_save: broadcast(type='app', task=tasks.symlink_project, args=[self.pk]) except Exception: log.error('failed to symlink project', exc_info=True) try: update_static_metadata(project_pk=self.pk) except Exception: log.error('failed to update static metadata', exc_info=True) try: branch = self.default_branch or self.vcs_repo().fallback_branch if not self.versions.filter(slug=LATEST).exists(): self.versions.create_latest(identifier=branch) except Exception: log.error('Error creating default branches', exc_info=True) def get_absolute_url(self): return reverse('projects_detail', args=[self.slug]) def get_docs_url(self, version_slug=None, lang_slug=None, private=None): """Return a url for the docs Always use http for now, to avoid content warnings. """ return resolve(project=self, version_slug=version_slug, language=lang_slug, private=private) def get_builds_url(self): return reverse('builds_project_list', kwargs={ 'project_slug': self.slug, }) def get_canonical_url(self): if getattr(settings, 'DONT_HIT_DB', True): return apiv2.project(self.pk).canonical_url().get()['url'] else: return self.get_docs_url() def get_subproject_urls(self): """List subproject URLs This is used in search result linking """ if getattr(settings, 'DONT_HIT_DB', True): return [(proj['slug'], proj['canonical_url']) for proj in ( apiv2.project(self.pk) .subprojects() .get()['subprojects'])] else: return [(proj.child.slug, proj.child.get_docs_url()) for proj in self.subprojects.all()] def get_production_media_path(self, type_, version_slug, include_file=True): """ This is used to see if these files exist so we can offer them for download. :param type_: Media content type, ie - 'pdf', 'zip' :param version_slug: Project version slug for lookup :param include_file: Include file name in return :type include_file: bool :returns: Full path to media file or path """ if getattr(settings, 'DEFAULT_PRIVACY_LEVEL', 'public') == 'public' or settings.DEBUG: path = os.path.join( settings.MEDIA_ROOT, type_, self.slug, version_slug) else: path = os.path.join( settings.PRODUCTION_MEDIA_ARTIFACTS, type_, self.slug, version_slug) if include_file: path = os.path.join( path, '%s.%s' % (self.slug, type_.replace('htmlzip', 'zip'))) return path def get_production_media_url(self, type_, version_slug, full_path=True): """Get the URL for downloading a specific media file.""" path = reverse('project_download_media', kwargs={ 'project_slug': self.slug, 'type_': type_, 'version_slug': version_slug, }) if full_path: path = '//%s%s' % (settings.PRODUCTION_DOMAIN, path) return path def get_downloads(self): downloads = {} downloads['htmlzip'] = self.get_production_media_url( 'htmlzip', self.get_default_version()) downloads['epub'] = self.get_production_media_url( 'epub', self.get_default_version()) downloads['pdf'] = self.get_production_media_url( 'pdf', self.get_default_version()) return downloads @property def clean_repo(self): if self.repo.startswith('http://github.com'): return self.repo.replace('http://github.com', 'https://github.com') return self.repo # Doc PATH: # MEDIA_ROOT/slug/checkouts/version/<repo> @property def doc_path(self): return os.path.join(settings.DOCROOT, self.slug.replace('_', '-')) def checkout_path(self, version=LATEST): return os.path.join(self.doc_path, 'checkouts', version) @property def pip_cache_path(self): """Path to pip cache""" return os.path.join(self.doc_path, '.cache', 'pip') # # Paths for symlinks in project doc_path. # def translations_symlink_path(self, language=None): """Path in the doc_path that we symlink translations""" if not language: language = self.language return os.path.join(self.doc_path, 'translations', language) # # End symlink paths # def full_doc_path(self, version=LATEST): """The path to the documentation root in the project""" doc_base = self.checkout_path(version) for possible_path in ['docs', 'doc', 'Doc']: if os.path.exists(os.path.join(doc_base, '%s' % possible_path)): return os.path.join(doc_base, '%s' % possible_path) # No docs directory, docs are at top-level. return doc_base def artifact_path(self, type_, version=LATEST): """The path to the build html docs in the project""" return os.path.join(self.doc_path, "artifacts", version, type_) def full_build_path(self, version=LATEST): """The path to the build html docs in the project""" return os.path.join(self.conf_dir(version), "_build", "html") def full_latex_path(self, version=LATEST): """The path to the build LaTeX docs in the project""" return os.path.join(self.conf_dir(version), "_build", "latex") def full_epub_path(self, version=LATEST): """The path to the build epub docs in the project""" return os.path.join(self.conf_dir(version), "_build", "epub") # There is currently no support for building man/dash formats, but we keep # the support there for existing projects. They might have already existing # legacy builds. def full_man_path(self, version=LATEST): """The path to the build man docs in the project""" return os.path.join(self.conf_dir(version), "_build", "man") def full_dash_path(self, version=LATEST): """The path to the build dash docs in the project""" return os.path.join(self.conf_dir(version), "_build", "dash") def full_json_path(self, version=LATEST): """The path to the build json docs in the project""" if 'sphinx' in self.documentation_type: return os.path.join(self.conf_dir(version), "_build", "json") elif 'mkdocs' in self.documentation_type: return os.path.join(self.checkout_path(version), "_build", "json") def full_singlehtml_path(self, version=LATEST): """The path to the build singlehtml docs in the project""" return os.path.join(self.conf_dir(version), "_build", "singlehtml") def rtd_build_path(self, version=LATEST): """The destination path where the built docs are copied""" return os.path.join(self.doc_path, 'rtd-builds', version) def static_metadata_path(self): """The path to the static metadata JSON settings file""" return os.path.join(self.doc_path, 'metadata.json') def conf_file(self, version=LATEST): """Find a ``conf.py`` file in the project checkout""" if self.conf_py_file: conf_path = os.path.join(self.checkout_path(version), self.conf_py_file) if os.path.exists(conf_path): log.info('Inserting conf.py file path from model') return conf_path else: log.warning("Conf file specified on model doesn't exist") files = self.find('conf.py', version) if not files: files = self.full_find('conf.py', version) if len(files) == 1: return files[0] for filename in files: if filename.find('doc', 70) != -1: return filename # Having this be translatable causes this odd error: # ProjectImportError(<django.utils.functional.__proxy__ object at # 0x1090cded0>,) raise ProjectImportError( u"Conf File Missing. Please make sure you have a conf.py in your project.") def conf_dir(self, version=LATEST): conf_file = self.conf_file(version) if conf_file: return os.path.dirname(conf_file) @property def is_type_sphinx(self): """Is project type Sphinx""" return 'sphinx' in self.documentation_type @property def is_type_mkdocs(self): """Is project type Mkdocs""" return 'mkdocs' in self.documentation_type @property def is_imported(self): return bool(self.repo) @property def has_good_build(self): return self.builds.filter(success=True).exists() @property def has_versions(self): return self.versions.exists() @property def has_aliases(self): return self.aliases.exists() def has_pdf(self, version_slug=LATEST): if not self.enable_pdf_build: return False return os.path.exists(self.get_production_media_path( type_='pdf', version_slug=version_slug)) def has_epub(self, version_slug=LATEST): if not self.enable_epub_build: return False return os.path.exists(self.get_production_media_path( type_='epub', version_slug=version_slug)) def has_htmlzip(self, version_slug=LATEST): return os.path.exists(self.get_production_media_path( type_='htmlzip', version_slug=version_slug)) @property def sponsored(self): return False def vcs_repo(self, version=LATEST): backend = backend_cls.get(self.repo_type) if not backend: repo = None else: proj = VCSProject( self.name, self.default_branch, self.checkout_path(version), self.clean_repo) repo = backend(proj, version) return repo def repo_nonblockinglock(self, version, max_lock_age=5): return NonBlockingLock(project=self, version=version, max_lock_age=max_lock_age) def repo_lock(self, version, timeout=5, polling_interval=5): return Lock(self, version, timeout, polling_interval) def find(self, filename, version): """Find files inside the project's ``doc`` path :param filename: Filename to search for in project checkout :param version: Version instance to set version checkout path """ matches = [] for root, __, filenames in os.walk(self.full_doc_path(version)): for filename in fnmatch.filter(filenames, filename): matches.append(os.path.join(root, filename)) return matches def full_find(self, filename, version): """Find files inside a project's checkout path :param filename: Filename to search for in project checkout :param version: Version instance to set version checkout path """ matches = [] for root, __, filenames in os.walk(self.checkout_path(version)): for filename in fnmatch.filter(filenames, filename): matches.append(os.path.join(root, filename)) return matches def get_latest_build(self, finished=True): """ Get latest build for project finished Return only builds that are in a finished state """ kwargs = {'type': 'html'} if finished: kwargs['state'] = 'finished' return self.builds.filter(**kwargs).first() def api_versions(self): ret = [] for version_data in api.version.get(project=self.pk, active=True)['objects']: version = make_api_version(version_data) ret.append(version) return sort_version_aware(ret) def active_versions(self): from readthedocs.builds.models import Version versions = Version.objects.public(project=self, only_active=True) return (versions.filter(built=True, active=True) | versions.filter(active=True, uploaded=True)) def ordered_active_versions(self, user=None): from readthedocs.builds.models import Version kwargs = { 'project': self, 'only_active': True, } if user: kwargs['user'] = user versions = Version.objects.public(**kwargs) return sort_version_aware(versions) def all_active_versions(self): """Get queryset with all active versions .. note:: This is a temporary workaround for activate_versions filtering out things that were active, but failed to build :returns: :py:cls:`Version` queryset """ return self.versions.filter(active=True) def supported_versions(self): """Get the list of supported versions :returns: List of version strings. """ if not self.num_major or not self.num_minor or not self.num_point: return [] version_identifiers = self.versions.values_list('verbose_name', flat=True) return version_windows( version_identifiers, major=self.num_major, minor=self.num_minor, point=self.num_point, ) def get_stable_version(self): return self.versions.filter(slug=STABLE).first() def update_stable_version(self): """Returns the version that was promoted to be the new stable version Return ``None`` if no update was mode or if there is no version on the project that can be considered stable. """ versions = self.versions.all() new_stable = determine_stable_version(versions) if new_stable: current_stable = self.get_stable_version() if current_stable: identifier_updated = ( new_stable.identifier != current_stable.identifier) if identifier_updated and current_stable.machine: log.info( "Update stable version: {project}:{version}".format( project=self.slug, version=new_stable.identifier)) current_stable.identifier = new_stable.identifier current_stable.save() return new_stable else: log.info( "Creating new stable version: {project}:{version}".format( project=self.slug, version=new_stable.identifier)) current_stable = self.versions.create_stable( type=new_stable.type, identifier=new_stable.identifier) return new_stable def version_from_branch_name(self, branch): versions = self.versions_from_branch_name(branch) try: return versions[0] except IndexError: return None def versions_from_branch_name(self, branch): return ( self.versions.filter(identifier=branch) | self.versions.filter(identifier='remotes/origin/%s' % branch) | self.versions.filter(identifier='origin/%s' % branch) ) def get_default_version(self): """ Get the default version (slug). Returns self.default_version if the version with that slug actually exists (is built and published). Otherwise returns 'latest'. """ # latest is a special case where we don't have to check if it exists if self.default_version == LATEST: return self.default_version # check if the default_version exists version_qs = self.versions.filter( slug=self.default_version, active=True ) if version_qs.exists(): return self.default_version return LATEST def get_default_branch(self): """Get the version representing 'latest'""" if self.default_branch: return self.default_branch else: return self.vcs_repo().fallback_branch def add_subproject(self, child, alias=None): subproject, __ = ProjectRelationship.objects.get_or_create( parent=self, child=child, alias=alias, ) return subproject def remove_subproject(self, child): ProjectRelationship.objects.filter(parent=self, child=child).delete() return def moderation_queue(self): # non-optimal SQL warning. from readthedocs.comments.models import DocumentComment queue = [] comments = DocumentComment.objects.filter(node__project=self) for comment in comments: if not comment.has_been_approved_since_most_recent_node_change(): queue.append(comment) return queue def add_node(self, content_hash, page, version, commit): """Add comment node :param content_hash: Hash of node content :param page: Doc page for node :param version: Slug for project version to apply node to :type version: str :param commit: Commit that node was updated in :type commit: str """ from readthedocs.comments.models import NodeSnapshot, DocumentNode project_obj = Project.objects.get(slug=self.slug) version_obj = project_obj.versions.get(slug=version) try: NodeSnapshot.objects.get(hash=content_hash, node__project=project_obj, node__version=version_obj, node__page=page, commit=commit) return False # ie, no new node was created. except NodeSnapshot.DoesNotExist: DocumentNode.objects.create( hash=content_hash, page=page, project=project_obj, version=version_obj, commit=commit ) return True # ie, it's True that a new node was created. def add_comment(self, version_slug, page, content_hash, commit, user, text): """Add comment to node :param version_slug: Version slug to use for node lookup :param page: Page to attach comment to :param content_hash: Hash of content to apply comment to :param commit: Commit that updated comment :param user: :py:cls:`User` instance that created comment :param text: Comment text """ from readthedocs.comments.models import DocumentNode try: node = self.nodes.from_hash(version_slug, page, content_hash) except DocumentNode.DoesNotExist: version = self.versions.get(slug=version_slug) node = self.nodes.create(version=version, page=page, hash=content_hash, commit=commit) return node.comments.create(user=user, text=text)
class BlogPost(models.Model): class Meta: # ordered by pub_date descending when retriving ordering = ['-pub_date'] def get_upload_md_name(self, filename): if self.pub_date: year = self.pub_date.year # always store in pub_year folder else: year = datetime.now().year upload_to = upload_dir % (year, self.title + '.md') return upload_to def get_html_name(self, filename): if self.pub_date: year = self.pub_date.year else: year = datetime.now().year upload_to = upload_dir % (year, filename) return upload_to CATEGORY_CHOICES = ( ('programming', 'Programming'), ('acg', 'Anime & Manga & Novel & Game'), ('nc', 'No Category'), ) title = models.CharField(max_length=150) body = models.TextField(blank=True) # uploaded md file md_file = models.FileField(upload_to=get_upload_md_name, blank=True) pub_date = models.DateTimeField('date published', auto_now_add=True) last_edit_date = models.DateTimeField('last edited', auto_now=True) slug = models.SlugField(max_length=200, blank=True) # generated html file html_file = models.FileField(upload_to=get_html_name, blank=True) category = models.CharField(max_length=30, choices=CATEGORY_CHOICES) description = models.TextField(blank=True) tags = TaggableManager() def __str__(self): return self.title # 根据继承搜索流程,先是实例属性,然后就是类属性,所以这样用没问题 @property def filename(self): if self.md_file: return os.path.basename(self.title) else: return 'no md_file' def save(self, *args, **kwargs): self.slug = slugify(unidecode(self.title)) if not self.body and self.md_file: self.body = self.md_file.read() html = markdown2.markdown( self.body, extras=["fenced-code-blocks", "tables", "toc", "header-ids"]) if html.toc_html: content_file = ContentFile( html.toc_html.encode('utf-8') + html.encode('utf-8')) else: content_file = ContentFile(html.encode('utf-8')) self.html_file.save(self.title + '.html', content_file, save=False) self.html_file.close() super().save(*args, **kwargs) def display_html(self): with open(self.html_file.path, encoding='utf-8') as f: return f.read() def get_absolute_url(self): from . import views return reverse(views.blogpost, kwargs={ 'slug': self.slug, 'post_id': self.id })
class ResourceBase(PolymorphicModel, PermissionLevelMixin): """ Base Resource Object loosely based on ISO 19115:2003 """ VALID_DATE_TYPES = [(x.lower(), _(x)) for x in ['Creation', 'Publication', 'Revision']] date_help_text = _('reference date for the cited resource') date_type_help_text = _('identification of when a given event occurred') edition_help_text = _('version of the cited resource') abstract_help_text = _( 'brief narrative summary of the content of the resource(s)') purpose_help_text = _( 'summary of the intentions with which the resource(s) was developed') maintenance_frequency_help_text = _( 'frequency with which modifications and deletions are made to the data after ' 'it is first produced') keywords_help_text = _( 'commonly used word(s) or formalised word(s) or phrase(s) used to describe the subject ' '(space or comma-separated') regions_help_text = _('keyword identifies a location') restriction_code_type_help_text = _( 'limitation(s) placed upon the access or use of the data.') constraints_other_help_text = _( 'other restrictions and legal prerequisites for accessing and using the resource or' ' metadata') license_help_text = _('license of the dataset') language_help_text = _('language used within the dataset') category_help_text = _( 'high-level geographic data thematic classification to assist in the grouping and search of ' 'available geographic data sets.') spatial_representation_type_help_text = _( 'method used to represent geographic information in the dataset.') temporal_extent_start_help_text = _( 'time period covered by the content of the dataset (start)') temporal_extent_end_help_text = _( 'time period covered by the content of the dataset (end)') distribution_url_help_text = _( 'information about on-line sources from which the dataset, specification, or ' 'community profile name and extended metadata elements can be obtained' ) distribution_description_help_text = _( 'detailed text description of what the online resource is/does') data_quality_statement_help_text = _( 'general explanation of the data producer\'s knowledge about the lineage of a' ' dataset') # internal fields uuid = models.CharField(max_length=36) owner = models.ForeignKey(settings.AUTH_USER_MODEL, blank=True, null=True, related_name='owned_resource', verbose_name=_("Owner")) contacts = models.ManyToManyField(settings.AUTH_USER_MODEL, through='ContactRole') title = models.CharField( _('title'), max_length=255, help_text=_('name by which the cited resource is known')) date = models.DateTimeField(_('date'), default=datetime.datetime.now, help_text=date_help_text) date_type = models.CharField(_('date type'), max_length=255, choices=VALID_DATE_TYPES, default='publication', help_text=date_type_help_text) edition = models.CharField(_('edition'), max_length=255, blank=True, null=True, help_text=edition_help_text) abstract = models.TextField(_('abstract'), blank=True, help_text=abstract_help_text) purpose = models.TextField(_('purpose'), null=True, blank=True, help_text=purpose_help_text) maintenance_frequency = models.CharField( _('maintenance frequency'), max_length=255, choices=UPDATE_FREQUENCIES, blank=True, null=True, help_text=maintenance_frequency_help_text) keywords = TaggableManager(_('keywords'), blank=True, help_text=keywords_help_text) regions = models.ManyToManyField(Region, verbose_name=_('keywords region'), blank=True, null=True, help_text=regions_help_text) restriction_code_type = models.ForeignKey( RestrictionCodeType, verbose_name=_('restrictions'), help_text=restriction_code_type_help_text, null=True, blank=True, limit_choices_to=Q(is_choice=True)) constraints_other = models.TextField(_('restrictions other'), blank=True, null=True, help_text=constraints_other_help_text) license = models.ForeignKey(License, null=True, blank=True, verbose_name=_("License"), help_text=license_help_text) language = models.CharField(_('language'), max_length=3, choices=ALL_LANGUAGES, default='eng', help_text=language_help_text) category = models.ForeignKey(TopicCategory, null=True, blank=True, limit_choices_to=Q(is_choice=True), help_text=category_help_text) spatial_representation_type = models.ForeignKey( SpatialRepresentationType, null=True, blank=True, limit_choices_to=Q(is_choice=True), verbose_name=_("spatial representation type"), help_text=spatial_representation_type_help_text) # Section 5 temporal_extent_start = models.DateTimeField( _('temporal extent start'), blank=True, null=True, help_text=temporal_extent_start_help_text) temporal_extent_end = models.DateTimeField( _('temporal extent end'), blank=True, null=True, help_text=temporal_extent_end_help_text) supplemental_information = models.TextField( _('supplemental information'), default=DEFAULT_SUPPLEMENTAL_INFORMATION, help_text=_('any other descriptive information about the dataset')) # Section 6 distribution_url = models.TextField(_('distribution URL'), blank=True, null=True, help_text=distribution_url_help_text) distribution_description = models.TextField( _('distribution description'), blank=True, null=True, help_text=distribution_description_help_text) # Section 8 data_quality_statement = models.TextField( _('data quality statement'), blank=True, null=True, help_text=data_quality_statement_help_text) # Section 9 # see metadata_author property definition below # Save bbox values in the database. # This is useful for spatial searches and for generating thumbnail images and metadata records. bbox_x0 = models.DecimalField(max_digits=19, decimal_places=10, blank=True, null=True) bbox_x1 = models.DecimalField(max_digits=19, decimal_places=10, blank=True, null=True) bbox_y0 = models.DecimalField(max_digits=19, decimal_places=10, blank=True, null=True) bbox_y1 = models.DecimalField(max_digits=19, decimal_places=10, blank=True, null=True) srid = models.CharField(max_length=255, default='EPSG:4326') # CSW specific fields csw_typename = models.CharField(_('CSW typename'), max_length=32, default='gmd:MD_Metadata', null=False) csw_schema = models.CharField(_('CSW schema'), max_length=64, default='http://www.isotc211.org/2005/gmd', null=False) csw_mdsource = models.CharField(_('CSW source'), max_length=256, default='local', null=False) csw_insert_date = models.DateTimeField(_('CSW insert date'), auto_now_add=True, null=True) csw_type = models.CharField(_('CSW type'), max_length=32, default='dataset', null=False, choices=HIERARCHY_LEVELS) csw_anytext = models.TextField(_('CSW anytext'), null=True, blank=True) csw_wkt_geometry = models.TextField( _('CSW WKT geometry'), null=False, default='POLYGON((-180 -90,-180 90,180 90,180 -90,-180 -90))') # metadata XML specific fields metadata_uploaded = models.BooleanField(default=False) metadata_uploaded_preserve = models.BooleanField(default=False) metadata_xml = models.TextField( null=True, default= '<gmd:MD_Metadata xmlns:gmd="http://www.isotc211.org/2005/gmd"/>', blank=True) popular_count = models.IntegerField(default=0) share_count = models.IntegerField(default=0) featured = models.BooleanField( _("Featured"), default=False, help_text=_('Should this resource be advertised in home page?')) is_published = models.BooleanField( _("Is Published"), default=True, help_text=_('Should this resource be published and searchable?')) # fields necessary for the apis thumbnail_url = models.TextField(null=True, blank=True) detail_url = models.CharField(max_length=255, null=True, blank=True) rating = models.IntegerField(default=0, null=True, blank=True) def __unicode__(self): return self.title @property def bbox(self): return [ self.bbox_x0, self.bbox_y0, self.bbox_x1, self.bbox_y1, self.srid ] @property def bbox_string(self): return ",".join([ str(self.bbox_x0), str(self.bbox_y0), str(self.bbox_x1), str(self.bbox_y1) ]) @property def geographic_bounding_box(self): return bbox_to_wkt(self.bbox_x0, self.bbox_x1, self.bbox_y0, self.bbox_y1, srid=self.srid) @property def license_light(self): a = [] if (not (self.license.name is None)) and (len(self.license.name) > 0): a.append(self.license.name) if (not (self.license.url is None)) and (len(self.license.url) > 0): a.append("(" + self.license.url + ")") return " ".join(a) @property def license_verbose(self): a = [] if (not (self.license.name_long is None)) and (len( self.license.name_long) > 0): a.append(self.license.name_long + ":") if (not (self.license.description is None)) and (len( self.license.description) > 0): a.append(self.license.description) if (not (self.license.url is None)) and (len(self.license.url) > 0): a.append("(" + self.license.url + ")") return " ".join(a) def keyword_list(self): return [kw.name for kw in self.keywords.all()] def keyword_slug_list(self): return [kw.slug for kw in self.keywords.all()] def region_name_list(self): return [region.name for region in self.regions.all()] def spatial_representation_type_string(self): if hasattr(self.spatial_representation_type, 'identifier'): return self.spatial_representation_type.identifier else: if hasattr(self, 'storeType'): if self.storeType == 'coverageStore': return 'grid' return 'vector' else: return None @property def keyword_csv(self): keywords_qs = self.get_real_instance().keywords.all() if keywords_qs: return ','.join([kw.name for kw in keywords_qs]) else: return '' def set_latlon_bounds(self, box): """ Set the four bounds in lat lon projection """ self.bbox_x0 = box[0] self.bbox_x1 = box[1] self.bbox_y0 = box[2] self.bbox_y1 = box[3] def set_bounds_from_center_and_zoom(self, center_x, center_y, zoom): """ Calculate zoom level and center coordinates in mercator. """ self.center_x = center_x self.center_y = center_y self.zoom = zoom deg_len_equator = 40075160 / 360 # covert center in lat lon def get_lon_lat(): wgs84 = Proj(init='epsg:4326') mercator = Proj(init='epsg:3857') lon, lat = transform(mercator, wgs84, center_x, center_y) return lon, lat # calculate the degree length at this latitude def deg_len(): lon, lat = get_lon_lat() return math.cos(lat) * deg_len_equator lon, lat = get_lon_lat() # taken from http://wiki.openstreetmap.org/wiki/Zoom_levels # it might be not precise but enough for the purpose distance_per_pixel = 40075160 * math.cos(lat) / 2**(zoom + 8) # calculate the distance from the center of the map in degrees # we use the calculated degree length on the x axis and the # normal degree length on the y axis assumin that it does not change # Assuming a map of 1000 px of width and 700 px of height distance_x_degrees = distance_per_pixel * 500 / deg_len() distance_y_degrees = distance_per_pixel * 350 / deg_len_equator self.bbox_x0 = lon - distance_x_degrees self.bbox_x1 = lon + distance_x_degrees self.bbox_y0 = lat - distance_y_degrees self.bbox_y1 = lat + distance_y_degrees def set_bounds_from_bbox(self, bbox): """ Calculate zoom level and center coordinates in mercator. """ self.set_latlon_bounds(bbox) minx, miny, maxx, maxy = [float(c) for c in bbox] x = (minx + maxx) / 2 y = (miny + maxy) / 2 (center_x, center_y) = forward_mercator((x, y)) xdiff = maxx - minx ydiff = maxy - miny zoom = 0 if xdiff > 0 and ydiff > 0: width_zoom = math.log(360 / xdiff, 2) height_zoom = math.log(360 / ydiff, 2) zoom = math.ceil(min(width_zoom, height_zoom)) self.zoom = zoom self.center_x = center_x self.center_y = center_y def download_links(self): """assemble download links for pycsw""" links = [] for url in self.link_set.all(): if url.link_type == 'metadata': # avoid recursion continue if url.link_type == 'html': links.append((self.title, 'Web address (URL)', 'WWW:LINK-1.0-http--link', url.url)) elif url.link_type in ('OGC:WMS', 'OGC:WFS', 'OGC:WCS'): links.append((self.title, url.name, url.link_type, url.url)) else: description = '%s (%s Format)' % (self.title, url.name) links.append((self.title, description, 'WWW:DOWNLOAD-1.0-http--download', url.url)) return links def get_tiles_url(self): """Return URL for Z/Y/X mapping clients or None if it does not exist. """ try: tiles_link = self.link_set.get(name='Tiles') except Link.DoesNotExist: return None else: return tiles_link.url def get_legend(self): """Return Link for legend or None if it does not exist. """ try: legends_link = self.link_set.get(name='Legend') except Link.DoesNotExist: return None else: return legends_link def get_legend_url(self): """Return URL for legend or None if it does not exist. The legend can be either an image (for Geoserver's WMS) or a JSON object for ArcGIS. """ legend = self.get_legend() if legend is None: return None return legend.url def get_ows_url(self): """Return URL for OGC WMS server None if it does not exist. """ try: ows_link = self.link_set.get(name='OGC:WMS') except Link.DoesNotExist: return None else: return ows_link.url def get_thumbnail_url(self): """Return a thumbnail url. It could be a local one if it exists, a remote one (WMS GetImage) for example or a 'Missing Thumbnail' one. """ local_thumbnails = self.link_set.filter(name='Thumbnail') if local_thumbnails.count() > 0: return local_thumbnails[0].url remote_thumbnails = self.link_set.filter(name='Remote Thumbnail') if remote_thumbnails.count() > 0: return remote_thumbnails[0].url return staticfiles.static(settings.MISSING_THUMBNAIL) def has_thumbnail(self): """Determine if the thumbnail object exists and an image exists""" return self.link_set.filter(name='Thumbnail').exists() def save_thumbnail(self, filename, image): thumb_folder = 'thumbs' upload_path = os.path.join(settings.MEDIA_ROOT, thumb_folder) if not os.path.exists(upload_path): os.makedirs(upload_path) with open(os.path.join(upload_path, filename), 'wb') as f: thumbnail = File(f) thumbnail.write(image) url_path = os.path.join(settings.MEDIA_URL, thumb_folder, filename).replace('\\', '/') url = urljoin(settings.SITEURL, url_path) Link.objects.get_or_create(resource=self, url=url, defaults=dict( name='Thumbnail', extension='png', mime='image/png', link_type='image', )) ResourceBase.objects.filter(id=self.id).update(thumbnail_url=url) def set_missing_info(self): """Set default permissions and point of contacts. It is mandatory to call it from descendant classes but hard to enforce technically via signals or save overriding. """ from guardian.models import UserObjectPermission logger.debug('Checking for permissions.') # True if every key in the get_all_level_info dict is empty. no_custom_permissions = UserObjectPermission.objects.filter( content_type=ContentType.objects.get_for_model( self.get_self_resource()), object_pk=str(self.pk)).exists() if not no_custom_permissions: logger.debug( 'There are no permissions for this object, setting default perms.' ) self.set_default_permissions() if self.owner: user = self.owner else: user = ResourceBase.objects.admin_contact().user if self.poc is None: self.poc = user if self.metadata_author is None: self.metadata_author = user def maintenance_frequency_title(self): return [ v for i, v in enumerate(UPDATE_FREQUENCIES) if v[0] == self.maintenance_frequency ][0][1].title() def language_title(self): return [ v for i, v in enumerate(ALL_LANGUAGES) if v[0] == self.language ][0][1].title() def _set_poc(self, poc): # reset any poc assignation to this resource ContactRole.objects.filter(role='pointOfContact', resource=self).delete() # create the new assignation ContactRole.objects.create(role='pointOfContact', resource=self, contact=poc) def _get_poc(self): try: the_poc = ContactRole.objects.get(role='pointOfContact', resource=self).contact except ContactRole.DoesNotExist: the_poc = None return the_poc poc = property(_get_poc, _set_poc) def _set_metadata_author(self, metadata_author): # reset any metadata_author assignation to this resource ContactRole.objects.filter(role='author', resource=self).delete() # create the new assignation ContactRole.objects.create(role='author', resource=self, contact=metadata_author) def _get_metadata_author(self): try: the_ma = ContactRole.objects.get(role='author', resource=self).contact except ContactRole.DoesNotExist: the_ma = None return the_ma metadata_author = property(_get_metadata_author, _set_metadata_author) objects = ResourceBaseManager() class Meta: # custom permissions, # add, change and delete are standard in django-guardian permissions = ( ('view_resourcebase', 'Can view resource'), ('change_resourcebase_permissions', 'Can change resource permissions'), ('download_resourcebase', 'Can download resource'), ('publish_resourcebase', 'Can publish resource'), ('change_resourcebase_metadata', 'Can change resource metadata'), )
class AbstractDocument(CollectionMember, index.Indexed, models.Model): title = models.CharField(max_length=255, verbose_name=_('title')) file = models.FileField(upload_to='documents', verbose_name=_('file')) created_at = models.DateTimeField(verbose_name=_('created at'), auto_now_add=True) Info = models.CharField(verbose_name='信息', max_length=255, blank=True) ClothCode = models.CharField(max_length=255, verbose_name="布匹编号", blank=True, default="000000") BatchNum = models.CharField(max_length=255, verbose_name="产品批号", blank=True, default="请上传说明文档") Specs = models.CharField(max_length=255, verbose_name="产品规格", blank=True, default="请上传说明文档") BatchNum = models.CharField(max_length=255, verbose_name="产品批号", blank=True, default="请上传说明文档") uploaded_by_user = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name=_('uploaded by user'), null=True, blank=True, editable=False, on_delete=models.SET_NULL) tags = TaggableManager(help_text=None, blank=True, verbose_name=_('tags')) file_size = models.PositiveIntegerField(null=True, editable=False) # A SHA-1 hash of the file contents file_hash = models.CharField(max_length=40, blank=True, editable=False) objects = DocumentQuerySet.as_manager() search_fields = CollectionMember.search_fields + [ index.SearchField('title', partial_match=True, boost=10), index.AutocompleteField('title'), index.FilterField('title'), index.RelatedFields('tags', [ index.SearchField('name', partial_match=True, boost=10), index.AutocompleteField('name'), ]), index.FilterField('uploaded_by_user'), ] @contextmanager def open_file(self): # Open file if it is closed close_file = False f = self.file if f.closed: # Reopen the file if self.is_stored_locally(): f.open('rb') else: # Some external storage backends don't allow reopening # the file. Get a fresh file instance. #1397 storage = self._meta.get_field('file').storage f = storage.open(f.name, 'rb') close_file = True # Seek to beginning f.seek(0) try: yield f finally: if close_file: f.close() def get_file_size(self): if self.file_size is None: try: self.file_size = self.file.size except Exception: # File doesn't exist return self.save(update_fields=['file_size']) return self.file_size def _set_file_hash(self, file_contents): self.file_hash = hashlib.sha1(file_contents).hexdigest() def get_file_hash(self): if self.file_hash == '': with self.open_file() as f: self._set_file_hash(f.read()) self.save(update_fields=['file_hash']) return self.file_hash def __str__(self): return self.title @property def filename(self): return os.path.basename(self.file.name) @property def file_extension(self): return os.path.splitext(self.filename)[1][1:] @property def url(self): return reverse('wagtaildocs_serve', args=[self.id, self.filename]) def get_usage(self): return get_object_usage(self) @property def usage_url(self): return reverse('wagtaildocs:document_usage', args=(self.id, )) def is_editable_by_user(self, user): from wagtail.documents.permissions import permission_policy return permission_policy.user_has_permission_for_instance( user, 'change', self) class Meta: abstract = True verbose_name = _('document') verbose_name_plural = _('documents')
class FoiRequest(models.Model): STATUS = Status RESOLUTION = Resolution FILTER_STATUS = FilterStatus VISIBILITY = Visibility STATUS_RESOLUTION_DICT = STATUS_RESOLUTION_DICT # model fields title = models.CharField(_("Title"), max_length=255) slug = models.SlugField(_("Slug"), max_length=255, unique=True) description = models.TextField(_("Description"), blank=True) summary = models.TextField(_("Summary"), blank=True) public_body = models.ForeignKey(PublicBody, null=True, blank=True, on_delete=models.SET_NULL, verbose_name=_("Public Body")) status = models.CharField( _("Status"), max_length=50, choices=Status.choices ) resolution = models.CharField( _("Resolution"), max_length=50, choices=Resolution.choices, blank=True ) public = models.BooleanField(_("published?"), default=True) visibility = models.SmallIntegerField( _("Visibility"), default=Visibility.INVISIBLE, choices=Visibility.choices ) user = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, on_delete=models.SET_NULL, verbose_name=_("User")) team = models.ForeignKey(Team, null=True, blank=True, on_delete=models.SET_NULL, verbose_name=_("Team")) first_message = models.DateTimeField(_("Date of first message"), blank=True, null=True) last_message = models.DateTimeField(_("Date of last message"), blank=True, null=True) resolved_on = models.DateTimeField(_("Resolution date"), blank=True, null=True) due_date = models.DateTimeField(_("Due Date"), blank=True, null=True) secret_address = models.CharField(_("Secret address"), max_length=255, db_index=True, unique=True) secret = models.CharField(_("Secret"), blank=True, max_length=100) reference = models.CharField(_("Reference"), blank=True, max_length=255) same_as = models.ForeignKey('self', null=True, blank=True, on_delete=models.SET_NULL, verbose_name=_("Identical request")) same_as_count = models.IntegerField(_("Identical request count"), default=0) project = models.ForeignKey(FoiProject, null=True, blank=True, on_delete=models.SET_NULL, verbose_name=_('project')) project_order = models.IntegerField(null=True, blank=True) law = models.ForeignKey(FoiLaw, null=True, blank=True, on_delete=models.SET_NULL, verbose_name=_("Freedom of Information Law")) costs = models.FloatField(_("Cost of Information"), default=0.0) refusal_reason = models.CharField(_("Refusal reason"), max_length=1024, blank=True) checked = models.BooleanField(_("checked"), default=False) is_blocked = models.BooleanField(_("Blocked"), default=False) not_publishable = models.BooleanField(_('Not publishable'), default=False) is_foi = models.BooleanField(_("is FoI request"), default=True) closed = models.BooleanField(_('is closed'), default=False) campaign = models.ForeignKey( Campaign, verbose_name=_('campaign'), null=True, blank=True, on_delete=models.SET_NULL ) jurisdiction = models.ForeignKey( Jurisdiction, verbose_name=_('Jurisdiction'), null=True, on_delete=models.SET_NULL ) site = models.ForeignKey(Site, null=True, on_delete=models.SET_NULL, verbose_name=_("Site")) non_filtered_objects = models.Manager() objects = FoiRequestManager() published = PublishedFoiRequestManager() published_not_foi = PublishedNotFoiRequestManager() tags = TaggableManager(through=TaggedFoiRequest, blank=True) class Meta: ordering = ('-last_message',) get_latest_by = 'last_message' verbose_name = _('Freedom of Information Request') verbose_name_plural = _('Freedom of Information Requests') permissions = ( ("see_private", _("Can see private requests")), ("create_batch", _("Create batch requests")), ("moderate", _("Can moderate requests")), ) # Custom Signals message_sent = django.dispatch.Signal(providing_args=["message", "user"]) message_received = django.dispatch.Signal(providing_args=["message"]) request_created = django.dispatch.Signal(providing_args=[]) request_to_public_body = django.dispatch.Signal(providing_args=[]) status_changed = django.dispatch.Signal(providing_args=[ "status", "resolution", "data", "user", "previous_status", "previous_resolution" ]) became_overdue = django.dispatch.Signal(providing_args=[]) became_asleep = django.dispatch.Signal(providing_args=[]) public_body_suggested = django.dispatch.Signal(providing_args=["suggestion"]) set_concrete_law = django.dispatch.Signal(providing_args=['name', 'user']) made_public = django.dispatch.Signal(providing_args=['user']) made_private = django.dispatch.Signal(providing_args=['user']) escalated = django.dispatch.Signal(providing_args=['message', 'user']) def __str__(self): return _("Request '%s'") % self.title @property def same_as_set(self): return FoiRequest.objects.filter(same_as=self) @property def messages(self): if not hasattr(self, "_messages") or self._messages is None: self.get_messages() return self._messages def get_messages(self, with_tags=False): qs = self.foimessage_set.select_related( "sender_user", "sender_public_body", "recipient_public_body" ).order_by("timestamp") if with_tags: qs = qs.prefetch_related('tags') self._messages = list(qs) return self._messages @property def get_messages_by_month(self): """ Group messages by "month-year"-key, e.g. "2020-09". Add extra due date key. """ groups = {} today = datetime.today() due_date = self.due_date month_highlighted = False for msg in self.messages: key = str(msg.timestamp)[:7] if key not in groups: groups[key] = { 'date': msg.timestamp.replace(day=1, hour=0, minute=0, second=0, microsecond=0), 'is_same_year': msg.timestamp.year == today.year, 'messages': [], 'has_overdue_message': False, 'highlight_overdue': False, 'first_message_id': msg.get_html_id } groups[key]['messages'].append(msg) if msg.timestamp > due_date: groups[key]['has_overdue_message'] = True if month_highlighted is False: groups[key]['highlight_overdue'] = True month_highlighted = True return list(groups.values()) @property def status_representation(self): if self.due_date is not None: if self.is_overdue(): return FilterStatus.OVERDUE return self.status if self.status != Status.RESOLVED else self.resolution @property def status_settable(self): return self.awaits_classification() @property def project_number(self): if self.project_order is not None: return self.project_order + 1 return None @property def has_fee(self): return self.costs > 0 def identical_count(self): if self.same_as: return self.same_as.same_as_count return self.same_as_count def get_absolute_url(self): return reverse('foirequest-show', kwargs={'slug': self.slug}) def get_absolute_url_last_message(self): return self.get_absolute_url() + '#last' @property def url(self): return self.get_absolute_url() def get_absolute_short_url(self): return get_absolute_short_url(self.id) def get_absolute_domain_url(self): return "%s%s" % (settings.SITE_URL, self.get_absolute_url()) def get_absolute_domain_short_url(self): return get_absolute_domain_short_url(self.id) def get_auth_link(self): from ..auth import get_foirequest_auth_code return "%s%s" % (settings.SITE_URL, reverse('foirequest-auth', kwargs={"obj_id": self.id, "code": get_foirequest_auth_code(self) })) def get_upload_link(self): from ..auth import get_foirequest_upload_code return "%s%s" % (settings.SITE_URL, reverse('foirequest-publicbody_upload', kwargs={ "obj_id": self.id, "code": get_foirequest_upload_code(self) })) def get_accessible_link(self): if self.visibility == self.VISIBILITY.VISIBLE_TO_REQUESTER: return self.get_auth_link() return self.get_absolute_domain_short_url() def get_autologin_url(self): return self.user.get_autologin_url( self.get_absolute_short_url() ) def is_public(self): return self.visibility == self.VISIBILITY.VISIBLE_TO_PUBLIC def in_public_search_index(self): return ( self.is_public() and self.is_foi and self.same_as_id is None and (self.project_id is None or self.project_order == 0) ) def get_redaction_regexes(self): from ..utils import get_foi_mail_domains user = self.user domains = get_foi_mail_domains() email_regexes = [r'[\w\.\-]+@' + x for x in domains] FROIDE_CONFIG = settings.FROIDE_CONFIG user_regexes = [] if user.private: user_regexes = [ '%s %s' % (FROIDE_CONFIG['redact_salutation'], user.get_full_name()), '%s %s' % (FROIDE_CONFIG['redact_salutation'], user.last_name), user.get_full_name(), user.last_name, user.first_name ] all_regexes = email_regexes + user_regexes + user.address.splitlines() all_regexes = [re.escape(a) for a in all_regexes] return json.dumps([a.strip() for a in all_regexes if a.strip()]) def get_description(self): return redact_plaintext(self.description, user=self.user) def response_messages(self): return list(filter(lambda m: m.is_response, self.messages)) def sent_messages(self): return list(filter(lambda m: not m.is_response, self.messages)) def reply_received(self): return len(self.response_messages()) > 0 def message_needs_status(self): mes = list(filter(lambda m: m.status is None, self.response_messages())) if not mes: return None return mes[0] def status_is_final(self): return self.status == Status.RESOLVED def needs_public_body(self): return self.status == Status.PUBLICBODY_NEEDED def awaits_response(self): return self.status in (Status.AWAITING_RESPONSE, Status.ASLEEP) def is_actionable(self): return not self.needs_public_body() and ( self.is_overdue() or self.reply_received() ) @classmethod def get_throttle_config(cls): return settings.FROIDE_CONFIG.get('request_throttle', None) def should_apply_throttle(self): last_message = self.messages[-1] return not last_message.is_response or not self.is_actionable() def can_be_escalated(self): return self.law.mediator_id and self.is_actionable() def is_overdue(self): return self.was_overdue() and self.awaits_response() def is_successful(self): return self.resolution == Resolution.SUCCESSFUL def was_overdue(self): if self.due_date: return self.due_date < timezone.now() return False def has_been_refused(self): return self.resolution in (Resolution.REFUSED, Resolution.PARTIALLY_SUCCESSFUL) def awaits_classification(self): return self.status == Status.AWAITING_CLASSIFICATION def moderate_classification(self): return self.awaits_classification() and self.available_for_moderator_action() def available_for_moderator_action(self): ago = timezone.now() - MODERATOR_CLASSIFICATION_OFFSET return self.last_message < ago def set_awaits_classification(self): self.status = Status.AWAITING_CLASSIFICATION def follow_count(self): from froide.foirequestfollower.models import FoiRequestFollower return FoiRequestFollower.objects.filter( request=self, confirmed=True ).count() def public_date(self): if self.due_date: return self.due_date + timedelta(days=settings.FROIDE_CONFIG.get( 'request_public_after_due_days', 14)) return None def get_set_tags_form(self): from ..forms import TagFoiRequestForm return TagFoiRequestForm(tags=self.tags.all()) def get_status_form(self): from ..forms import FoiRequestStatusForm if self.status not in (Status.AWAITING_RESPONSE, Status.RESOLVED): status = '' else: status = self.status return FoiRequestStatusForm( foirequest=self, initial={ "status": status, 'resolution': self.resolution, "costs": self.costs, "refusal_reason": self.refusal_reason }) def public_body_suggestions_form(self): from ..forms import PublicBodySuggestionsForm return PublicBodySuggestionsForm(foirequest=self) def make_public_body_suggestion_form(self): from ..forms import MakePublicBodySuggestionForm return MakePublicBodySuggestionForm() def get_concrete_law_form(self): from ..forms import ConcreteLawForm return ConcreteLawForm(foirequest=self) def get_postal_reply_form(self): from ..forms import get_postal_reply_form return get_postal_reply_form(foirequest=self) def get_postal_message_form(self): from ..forms import get_postal_message_form return get_postal_message_form(foirequest=self) def get_send_message_form(self): from ..forms import get_send_message_form return get_send_message_form(foirequest=self) def get_escalation_message_form(self): from ..forms import get_escalation_message_form return get_escalation_message_form(foirequest=self) def quote_last_message(self): return list(self.messages)[-1].get_quoted() @property def readable_status(self): return FoiRequest.get_readable_status(self.status_representation) @property def status_description(self): return FoiRequest.get_status_description(self.status_representation) @classmethod def get_readable_status(cls, status, fallback=UNKNOWN_STATUS): return str(cls.STATUS_RESOLUTION_DICT.get(status, fallback).label) @classmethod def get_status_description(cls, status, fallback=UNKNOWN_STATUS): return str(cls.STATUS_RESOLUTION_DICT.get(status, fallback).description) def determine_visibility(self): if self.public: self.visibility = self.VISIBILITY.VISIBLE_TO_PUBLIC else: self.visibility = self.VISIBILITY.VISIBLE_TO_REQUESTER def set_status_after_change(self): if not self.user.is_active: self.status = Status.AWAITING_USER_CONFIRMATION else: self.determine_visibility() if self.public_body is None: self.status = Status.PUBLICBODY_NEEDED elif not self.public_body.confirmed: self.status = Status.AWAITING_PUBLICBODY_CONFIRMATION else: self.status = Status.AWAITING_RESPONSE return True return False def safe_send_first_message(self): messages = self.foimessage_set.all() if not len(messages) == 1: return None message = messages[0] if message.sent: return None message.send() self.message_sent.send( sender=self, message=message, user=self.user ) def confirmed_public_body(self): send_now = self.set_status_after_change() self.save() if send_now: self.safe_send_first_message() return True return False def suggest_public_body(self, public_body, reason, user): from .suggestion import PublicBodySuggestion try: PublicBodySuggestion.objects.get( public_body=public_body, request=self ) except PublicBodySuggestion.DoesNotExist: suggestion = self.publicbodysuggestion_set.create( public_body=public_body, reason=reason, user=user) self.public_body_suggested.send( sender=self, suggestion=suggestion ) return suggestion else: return False def make_public(self, user=None): self.public = True self.visibility = 2 self.save() self.made_public.send(sender=self, user=user) def set_overdue(self): self.became_overdue.send(sender=self) def set_asleep(self): self.status = Status.ASLEEP self.save() self.became_asleep.send(sender=self) def days_to_resolution(self): final = None mes = None resolutions = dict(Resolution.choices) for mes in self.response_messages(): if mes.status == Status.RESOLVED or mes.status in resolutions: final = mes.timestamp break if final is None or mes is None: return None return (mes.timestamp - self.first_message).days